From 7547a9b78a3c823b00461319dab39ddf1c8a31b9 Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Fri, 13 May 2022 00:55:52 +0900 Subject: [PATCH 01/14] Build 0.18.0.0 --- .../ClientSource/Characters/Character.cs | 2 +- .../Characters/Health/CharacterHealth.cs | 19 + .../ClientSource/Characters/Jobs/JobPrefab.cs | 10 +- .../ClientSource/Characters/Limb.cs | 102 ++-- .../Transition/UgcTransition.cs | 2 +- .../ClientSource/DebugConsole.cs | 54 +- .../ClientSource/GUI/GUITextBlock.cs | 5 +- .../ClientSource/GUI/GUITextBox.cs | 2 +- .../ClientSource/GUI/MedicalClinicUI.cs | 32 +- .../ClientSource/GUI/Store.cs | 99 +--- .../ClientSource/GUI/SubmarineSelection.cs | 4 +- .../ClientSource/GUI/TabMenu.cs | 560 ++++++++++-------- .../BarotraumaClient/ClientSource/GameMain.cs | 9 +- .../ClientSource/GameSession/CrewManager.cs | 9 +- .../ClientSource/GameSession/Data/Wallet.cs | 5 + .../GameModes/SinglePlayerCampaign.cs | 51 +- .../GameModes/Tutorials/DoctorTutorial.cs | 2 +- .../GameModes/Tutorials/EngineerTutorial.cs | 2 +- .../GameModes/Tutorials/MechanicTutorial.cs | 4 +- .../GameModes/Tutorials/ScenarioTutorial.cs | 2 +- .../ClientSource/Items/CharacterInventory.cs | 6 +- .../ClientSource/Items/Components/Door.cs | 9 +- .../Components/Machines/Deconstructor.cs | 21 +- .../Items/Components/Machines/Sonar.cs | 9 + .../Items/Components/RepairTool.cs | 19 +- .../Items/Components/Signal/Connection.cs | 31 +- .../Components/Signal/ConnectionPanel.cs | 24 +- .../Components/Signal/CustomInterface.cs | 2 +- .../Items/Components/Signal/Wire.cs | 2 +- .../ClientSource/Items/Components/Turret.cs | 2 +- .../ClientSource/Items/DockingPort.cs | 9 +- .../ClientSource/Items/Inventory.cs | 2 +- .../ClientSource/Items/Item.cs | 15 +- .../ClientSource/Map/ItemAssemblyPrefab.cs | 8 +- .../ClientSource/Map/Lights/ConvexHull.cs | 17 +- .../ClientSource/Map/RoundSound.cs | 9 +- .../ClientSource/Map/Submarine.cs | 2 +- .../ClientSource/Networking/ChatMessage.cs | 8 +- .../ClientSource/Networking/GameClient.cs | 85 +-- .../ClientSource/Networking/ServerSettings.cs | 36 +- .../ClientSource/Particles/Particle.cs | 10 +- .../ClientSource/Particles/ParticleEmitter.cs | 5 +- .../ClientSource/Particles/ParticleManager.cs | 4 +- .../ClientSource/Screens/MainMenuScreen.cs | 15 +- .../ClientSource/Screens/ModDownloadScreen.cs | 9 +- .../ClientSource/Screens/NetLobbyScreen.cs | 7 +- .../ClientSource/Screens/SubEditorScreen.cs | 74 ++- .../ClientSource/Sounds/SoundPlayer.cs | 12 +- .../ClientSource/Sprite/Sprite.cs | 14 +- .../ClientSource/Steam/Workshop.cs | 6 +- .../Steam/WorkshopMenu/Mutable/PublishTab.cs | 5 + .../ClientSource/Steam/WorkshopMenu/UiUtil.cs | 5 + .../Text/LocalizedString/LimitLString.cs | 2 - .../ClientSource/Utils/ToolBox.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 | 2 +- .../ServerSource/GameSession/CrewManager.cs | 10 +- .../GameSession/GameModes/CampaignMode.cs | 28 +- .../GameModes/MultiPlayerCampaign.cs | 83 +-- .../Items/Components/DockingPort.cs | 14 +- .../Items/Components/ItemLabel.cs | 2 +- .../Components/Signal/ConnectionPanel.cs | 11 +- .../Map/Creatures/BallastFloraBehavior.cs | 2 +- .../ServerSource/Networking/Client.cs | 2 +- .../Networking/FileTransfer/FileSender.cs | 2 +- .../ServerSource/Networking/GameServer.cs | 27 +- .../ServerSource/Networking/KarmaManager.cs | 4 +- .../ServerSource/Networking/RespawnManager.cs | 45 +- .../ServerSource/Networking/Voting.cs | 65 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Characters/AI/AIController.cs | 11 + .../Characters/AI/EnemyAIController.cs | 24 +- .../Characters/AI/HumanAIController.cs | 94 ++- .../AI/Objectives/AIObjectiveCleanupItems.cs | 2 +- .../AI/Objectives/AIObjectiveCombat.cs | 37 +- .../AI/Objectives/AIObjectiveFindSafety.cs | 312 ++++++---- .../AI/Objectives/AIObjectiveFixLeak.cs | 1 + .../AI/Objectives/AIObjectiveGetItem.cs | 30 +- .../AI/Objectives/AIObjectiveRescue.cs | 5 +- .../Characters/Animation/AnimController.cs | 18 +- .../Animation/HumanoidAnimController.cs | 4 +- .../SharedSource/Characters/Character.cs | 40 +- .../SharedSource/Characters/CharacterInfo.cs | 7 +- .../Health/Afflictions/AfflictionPrefab.cs | 1 + .../Characters/Health/CharacterHealth.cs | 35 +- .../SharedSource/Characters/HumanPrefab.cs | 4 +- .../SharedSource/Characters/Jobs/Job.cs | 7 +- .../SharedSource/Characters/Jobs/JobPrefab.cs | 2 +- .../SharedSource/Characters/Limb.cs | 20 +- .../ContentFile/BallastFloraFile.cs | 2 +- .../CaveGenerationParametersFile.cs | 2 +- .../ContentFile/CharacterFile.cs | 2 +- .../ContentFile/CorpsesFile.cs | 2 +- .../ContentFile/EventManagerSettingsFile.cs | 2 +- .../ContentFile/FactionsFile.cs | 2 +- .../ContentFile/GenericPrefabFile.cs | 12 +- .../ContentFile/ItemAssemblyFile.cs | 2 +- .../ContentManagement/ContentFile/ItemFile.cs | 2 +- .../ContentFile/LevelObjectPrefabsFile.cs | 2 +- .../ContentFile/LocationTypesFile.cs | 2 +- .../ContentFile/MissionsFile.cs | 2 +- .../ContentFile/NPCSetsFile.cs | 2 +- .../ContentFile/OrdersFile.cs | 2 +- .../ContentFile/OutpostConfigFile.cs | 2 +- .../ContentFile/ParticlesFile.cs | 2 +- .../ContentFile/RandomEventsFile.cs | 2 +- .../ContentFile/RuinConfigFile.cs | 2 +- .../ContentFile/SoundsFile.cs | 2 +- .../ContentFile/StartItemsFile.cs | 12 + .../ContentFile/StructureFile.cs | 2 +- .../ContentFile/TalentTreesFile.cs | 2 +- .../ContentFile/TalentsFile.cs | 2 +- .../ContentFile/TraitorMissionsFile.cs | 2 +- .../ContentFile/UpgradeModulesFile.cs | 2 +- .../ContentFile/WreckAIConfigFile.cs | 2 +- .../ContentPackage/ContentPackage.cs | 3 +- .../ContentManagement/ContentPath.cs | 5 +- .../ContentManagement/ContentXElement.cs | 2 +- .../MissingContentPackageException.cs | 2 +- .../SharedSource/DebugConsole.cs | 13 +- .../SharedSource/Events/ArtifactEvent.cs | 5 +- .../SharedSource/Events/Event.cs | 18 +- .../SharedSource/Events/EventManager.cs | 29 +- .../SharedSource/Events/EventSet.cs | 6 +- .../SharedSource/Events/MalfunctionEvent.cs | 10 +- .../SharedSource/Events/Missions/Mission.cs | 5 +- .../Events/Missions/MissionPrefab.cs | 34 +- .../Events/Missions/PirateMission.cs | 2 +- .../SharedSource/Events/MonsterEvent.cs | 109 ++-- .../SharedSource/Events/ScriptedEvent.cs | 10 +- .../GameSession/AutoItemPlacer.cs | 183 ++++-- .../SharedSource/GameSession/CargoManager.cs | 143 +++-- .../SharedSource/GameSession/CrewManager.cs | 14 +- .../GameSession/GameModes/CampaignMode.cs | 190 +++++- .../GameModes/MultiPlayerCampaign.cs | 2 +- .../SharedSource/GameSession/GameSession.cs | 38 +- .../SharedSource/InputType.cs | 1 - .../Items/Components/DockingPort.cs | 98 ++- .../SharedSource/Items/Components/Door.cs | 23 +- .../Items/Components/Holdable/Holdable.cs | 11 +- .../Components/Holdable/LevelResource.cs | 23 +- .../Items/Components/Holdable/MeleeWeapon.cs | 9 +- .../Items/Components/Holdable/Pickable.cs | 3 +- .../Items/Components/Holdable/Propulsion.cs | 11 +- .../Items/Components/ItemComponent.cs | 2 +- .../Items/Components/ItemContainer.cs | 8 +- .../Items/Components/Machines/Controller.cs | 48 +- .../Items/Components/Power/Powered.cs | 2 +- .../Items/Components/Projectile.cs | 11 + .../BooleanOperatorComponent/AndComponent.cs | 10 + .../BooleanOperatorComponent.cs} | 13 +- .../BooleanOperatorComponent/OrComponent.cs | 10 + .../BooleanOperatorComponent/XorComponent.cs | 10 + .../Items/Components/Signal/Connection.cs | 253 ++++---- .../Components/Signal/ConnectionPanel.cs | 32 +- .../Items/Components/Signal/LightComponent.cs | 2 +- .../Items/Components/Signal/OrComponent.cs | 33 -- .../Items/Components/Signal/WifiComponent.cs | 2 +- .../Items/Components/Signal/Wire.cs | 12 +- .../Items/Components/Signal/XorComponent.cs | 34 -- .../Items/Components/TriggerComponent.cs | 21 +- .../SharedSource/Items/Components/Turret.cs | 9 +- .../SharedSource/Items/Item.cs | 61 +- .../SharedSource/Items/ItemPrefab.cs | 31 +- .../Map/Creatures/BallastFloraBehavior.cs | 16 +- .../BarotraumaShared/SharedSource/Map/Gap.cs | 19 +- .../BarotraumaShared/SharedSource/Map/Hull.cs | 2 +- .../SharedSource/Map/Levels/CaveGenerator.cs | 15 +- .../SharedSource/Map/Levels/Level.cs | 136 +++-- .../SharedSource/Map/Levels/LevelData.cs | 16 +- .../Map/Levels/LevelGenerationParams.cs | 54 +- .../Levels/LevelObjects/LevelObjectManager.cs | 56 +- .../Levels/LevelObjects/LevelObjectPrefab.cs | 1 + .../Map/Levels/LevelObjects/LevelTrigger.cs | 1 - .../SharedSource/Map/Map/Location.cs | 80 +-- .../SharedSource/Map/Map/LocationType.cs | 33 -- .../SharedSource/Map/Map/Map.cs | 27 +- .../SharedSource/Map/MapEntity.cs | 26 +- .../Map/Outposts/OutpostGenerator.cs | 4 +- .../SharedSource/Map/Structure.cs | 27 +- .../SharedSource/Map/Submarine.cs | 365 ++++++------ .../SharedSource/Map/SubmarineBody.cs | 47 +- .../Networking/ChildServerRelay.cs | 15 + .../SharedSource/Networking/RespawnManager.cs | 17 +- .../SharedSource/Networking/ServerSettings.cs | 14 +- .../SharedSource/Physics/PhysicsBody.cs | 57 +- .../SharedSource/Prefabs/Prefab.cs | 4 +- .../SharedSource/Settings/GameSettings.cs | 33 +- .../StatusEffects/PropertyConditional.cs | 3 +- .../StatusEffects/StatusEffect.cs | 21 +- .../SharedSource/Steam/Workshop.cs | 26 +- .../SharedSource/Utils/AssemblyInfo.cs | 4 + .../SharedSource/Utils/ReflectionUtils.cs | 7 +- .../SharedSource/Utils/SaveUtil.cs | 2 +- Barotrauma/BarotraumaShared/changelog.txt | 84 +++ .../BarotraumaShared/serversettings.xml | 4 +- .../INetSerializableStructTests.cs | 205 +++++++ Barotrauma/BarotraumaTest/LinuxTest.csproj | 35 ++ Barotrauma/BarotraumaTest/MacTest.csproj | 35 ++ Barotrauma/BarotraumaTest/TestExample.cs | 122 ++++ Barotrauma/BarotraumaTest/TestProject.cs | 34 ++ Barotrauma/BarotraumaTest/WindowsTest.csproj | 35 ++ .../Common/PathManager.cs | 8 +- .../Common/PhysicsLogic/BreakableBody.cs | 8 +- .../Common/Serialization.cs | 2 +- .../Content/BodyContainer.cs | 2 +- .../Dynamics/Body.Factory.cs | 54 +- .../Dynamics/Fixture.cs | 26 +- .../Dynamics/World.Factory.cs | 121 ++-- .../Dynamics/World.cs | 26 +- .../Graphics/Texture2D.OpenGL.cs | 3 + LinuxSolution.sln | 15 + MacSolution.sln | 15 + WindowsSolution.sln | 9 + 218 files changed, 3881 insertions(+), 2192 deletions(-) create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/StartItemsFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/AndComponent.cs rename Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/{AndComponent.cs => BooleanOperatorComponent/BooleanOperatorComponent.cs} (90%) create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/OrComponent.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/XorComponent.cs delete mode 100644 Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs delete mode 100644 Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs create mode 100644 Barotrauma/BarotraumaTest/INetSerializableStructTests.cs create mode 100644 Barotrauma/BarotraumaTest/LinuxTest.csproj create mode 100644 Barotrauma/BarotraumaTest/MacTest.csproj create mode 100644 Barotrauma/BarotraumaTest/TestExample.cs create mode 100644 Barotrauma/BarotraumaTest/TestProject.cs create mode 100644 Barotrauma/BarotraumaTest/WindowsTest.csproj diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 0891e5008..3c015ee76 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -400,7 +400,7 @@ namespace Barotrauma partial void UpdateControlled(float deltaTime, Camera cam) { - if (controlled != this) return; + if (controlled != this) { return; } ControlLocalPlayer(deltaTime, cam); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 436e47141..91e852beb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -2008,8 +2008,27 @@ namespace Barotrauma DisplayedVitality = Vitality; } + partial void UpdateSkinTint() + { + if (!Character.IsVisible) { return; } + FaceTint = DefaultFaceTint; + BodyTint = Color.TransparentBlack; + + if (!(Character?.Params?.Health.ApplyAfflictionColors ?? false)) { return; } + + foreach (KeyValuePair kvp in afflictions) + { + var affliction = kvp.Key; + Color faceTint = affliction.GetFaceTint(); + if (faceTint.A > FaceTint.A) { FaceTint = faceTint; } + Color bodyTint = affliction.GetBodyTint(); + if (bodyTint.A > BodyTint.A) { BodyTint = bodyTint; } + } + } + partial void UpdateLimbAfflictionOverlays() { + if (!Character.IsVisible) { return; } foreach (Limb limb in Character.AnimController.Limbs) { if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs index 19640dbc7..1ac28a552 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs @@ -58,21 +58,19 @@ namespace Barotrauma public class OutfitPreview { - /// - /// Pair.First = sprite, Pair.Second = draw offset - /// - public readonly List> Sprites; + public readonly List<(Sprite sprite, Vector2 drawOffset)> Sprites; + public Vector2 Dimensions; public OutfitPreview() { - Sprites = new List>(); + Sprites = new List<(Sprite sprite, Vector2 drawOffset)>(); Dimensions = Vector2.One; } public void AddSprite(Sprite sprite, Vector2 drawOffset) { - Sprites.Add(new Pair(sprite, drawOffset)); + Sprites.Add((sprite, drawOffset)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 41d2d470e..7fbcef8fe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -224,14 +224,14 @@ namespace Barotrauma public float DamageOverlayStrength { get { return damageOverlayStrength; } - set { damageOverlayStrength = MathHelper.Clamp(value, 0.0f, 100.0f); } + set { damageOverlayStrength = MathHelper.Clamp(value, 0.0f, 1.0f); } } private float burnOverLayStrength; public float BurnOverlayStrength { get { return burnOverLayStrength; } - set { burnOverLayStrength = MathHelper.Clamp(value, 0.0f, 100.0f); } + set { burnOverLayStrength = MathHelper.Clamp(value, 0.0f, 1.0f); } } public string HitSoundTag => Params?.Sound?.Tag; @@ -279,7 +279,7 @@ namespace Barotrauma for (int i = 0; i < Params.decorativeSpriteParams.Count; i++) { var param = Params.decorativeSpriteParams[i]; - var decorativeSprite = new DecorativeSprite(param.Element, file: GetSpritePath(param.Element, param)); + var decorativeSprite = new DecorativeSprite(param.Element, file: GetSpritePath(param.Element, param, ref _texturePath)); DecorativeSprites.Add(decorativeSprite); int groupID = decorativeSprite.RandomGroupID; if (!DecorativeSpriteGroups.ContainsKey(groupID)) @@ -295,13 +295,13 @@ namespace Barotrauma switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": - Sprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.normalSpriteParams)); + Sprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.normalSpriteParams, ref _texturePath)); break; - case "damagedsprite": - DamagedSprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.damagedSpriteParams)); + case "damagedsprite": + DamagedSprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.damagedSpriteParams, ref _damagedTexturePath)); break; case "conditionalsprite": - var conditionalSprite = new ConditionalSprite(subElement, GetConditionalTarget(), file: GetSpritePath(subElement, null)); + var conditionalSprite = new ConditionalSprite(subElement, GetConditionalTarget(), file: GetSpritePath(subElement, null, ref _texturePath)); ConditionalSprites.Add(conditionalSprite); if (conditionalSprite.DeformableSprite != null) { @@ -311,7 +311,7 @@ namespace Barotrauma } break; case "deformablesprite": - _deformSprite = new DeformableSprite(subElement, filePath: GetSpritePath(subElement, Params.deformSpriteParams)); + _deformSprite = new DeformableSprite(subElement, filePath: GetSpritePath(subElement, Params.deformSpriteParams, ref _texturePath)); var deformations = CreateDeformations(subElement); Deformations.AddRange(deformations); NonConditionalDeformations.AddRange(deformations); @@ -435,33 +435,33 @@ namespace Barotrauma { Sprite.Remove(); var source = Sprite.SourceElement; - Sprite = new Sprite(source, file: GetSpritePath(source, Params.normalSpriteParams)); + Sprite = new Sprite(source, file: GetSpritePath(source, Params.normalSpriteParams, ref _texturePath)); } if (_deformSprite != null) { _deformSprite.Remove(); var source = _deformSprite.Sprite.SourceElement; - _deformSprite = new DeformableSprite(source, filePath: GetSpritePath(source, Params.deformSpriteParams)); + _deformSprite = new DeformableSprite(source, filePath: GetSpritePath(source, Params.deformSpriteParams, ref _texturePath)); } if (DamagedSprite != null) { DamagedSprite.Remove(); var source = DamagedSprite.SourceElement; - DamagedSprite = new Sprite(source, file: GetSpritePath(source, Params.damagedSpriteParams)); + DamagedSprite = new Sprite(source, file: GetSpritePath(source, Params.damagedSpriteParams, ref _damagedTexturePath)); } for (int i = 0; i < ConditionalSprites.Count; i++) { var conditionalSprite = ConditionalSprites[i]; var source = conditionalSprite.ActiveSprite.SourceElement; conditionalSprite.Remove(); - ConditionalSprites[i] = new ConditionalSprite(source, character, file: GetSpritePath(source, null)); + ConditionalSprites[i] = new ConditionalSprite(source, character, file: GetSpritePath(source, null, ref _texturePath)); } for (int i = 0; i < DecorativeSprites.Count; i++) { var decorativeSprite = DecorativeSprites[i]; decorativeSprite.Remove(); var source = decorativeSprite.Sprite.SourceElement; - DecorativeSprites[i] = new DecorativeSprite(source, file: GetSpritePath(source, Params.decorativeSpriteParams[i])); + DecorativeSprites[i] = new DecorativeSprite(source, file: GetSpritePath(source, Params.decorativeSpriteParams[i], ref _texturePath)); } } @@ -472,16 +472,17 @@ namespace Barotrauma } private string _texturePath; - private string GetSpritePath(ContentXElement element, SpriteParams spriteParams) + private string _damagedTexturePath; + private string GetSpritePath(ContentXElement element, SpriteParams spriteParams, ref string path) { - if (_texturePath == null) + if (path == null) { if (spriteParams != null) { ContentPath texturePath = character.Params.VariantFile?.Root?.GetAttributeContentPath("texture", character.Prefab.ContentPackage) ?? ContentPath.FromRaw(character.Prefab.ContentPackage, spriteParams.GetTexturePath()); - _texturePath = GetSpritePath(texturePath); + path = GetSpritePath(texturePath); } else { @@ -489,10 +490,10 @@ namespace Barotrauma texturePath = texturePath.IsNullOrWhiteSpace() ? ContentPath.FromRaw(character.Prefab.ContentPackage, ragdoll.RagdollParams.Texture) : texturePath; - _texturePath = GetSpritePath(texturePath); + path = GetSpritePath(texturePath); } } - return _texturePath; + return path; } /// @@ -625,12 +626,7 @@ namespace Barotrauma { if (!body.Enabled) { return; } - if (!IsDead) - { - DamageOverlayStrength -= deltaTime; - BurnOverlayStrength -= deltaTime; - } - else + if (IsDead) { var spriteParams = Params.GetSprite(); if (spriteParams != null && spriteParams.DeadColorTime > 0 && deadTimer < spriteParams.DeadColorTime) @@ -688,7 +684,7 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, Camera cam, Color? overrideColor = null, bool disableDeformations = false) { - float brightness = 1.0f - (burnOverLayStrength / 100.0f) * 0.5f; + float brightness = Math.Max(1.0f - burnOverLayStrength, 0.2f); var spriteParams = Params.GetSprite(); if (spriteParams == null) { return; } @@ -831,32 +827,6 @@ namespace Barotrauma { LightSource.LightSpriteEffect = (dir == Direction.Right) ? SpriteEffects.None : SpriteEffects.FlipVertically; } - if (damageOverlayStrength > 0.0f && DamagedSprite != null && !hideLimb) - { - DamagedSprite.Draw(spriteBatch, - new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), - color * Math.Min(damageOverlayStrength, 1.0f), activeSprite.Origin, - -body.DrawRotation, - Scale, spriteEffect, activeSprite.Depth - (depthStep * 90)); - } - foreach (var decorativeSprite in DecorativeSprites) - { - if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - Color c = new Color(decorativeSprite.Color.R / 255f * brightness, decorativeSprite.Color.G / 255f * brightness, decorativeSprite.Color.B / 255f * brightness, decorativeSprite.Color.A / 255f); - if (deadTimer > 0) - { - c = Color.Lerp(c, spriteParams.DeadColor, MathUtils.InverseLerp(0, Params.GetSprite().DeadColorTime, deadTimer)); - } - c = overrideColor ?? c; - float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier) * Scale; - var ca = (float)Math.Cos(-body.Rotation); - var sa = (float)Math.Sin(-body.Rotation); - Vector2 transformedOffset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y); - decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X + transformedOffset.X, -(body.DrawPosition.Y + transformedOffset.Y)), c, - -body.Rotation + rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, spriteEffect, - depth: activeSprite.Depth - (depthStep * 100)); - } float step = depthStep; WearableSprite onlyDrawable = wearingItems.Find(w => w.HideOtherWearables); if (Params.MirrorHorizontally) @@ -925,6 +895,36 @@ namespace Barotrauma //if there are multiple sprites on this limb, make the successive ones be drawn in front depthStep += step; } + if (!Hide && onlyDrawable == null) + { + foreach (var decorativeSprite in DecorativeSprites) + { + if (!spriteAnimState[decorativeSprite].IsActive) { continue; } + Color c = new Color(decorativeSprite.Color.R / 255f * brightness, decorativeSprite.Color.G / 255f * brightness, decorativeSprite.Color.B / 255f * brightness, decorativeSprite.Color.A / 255f); + if (deadTimer > 0) + { + c = Color.Lerp(c, spriteParams.DeadColor, MathUtils.InverseLerp(0, Params.GetSprite().DeadColorTime, deadTimer)); + } + c = overrideColor ?? c; + float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier) * Scale; + var ca = (float)Math.Cos(-body.Rotation); + var sa = (float)Math.Sin(-body.Rotation); + Vector2 transformedOffset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y); + decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X + transformedOffset.X, -(body.DrawPosition.Y + transformedOffset.Y)), c, + -body.Rotation + rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, spriteEffect, + depth: activeSprite.Depth - depthStep); + depthStep += step; + } + if (damageOverlayStrength > 0.0f && DamagedSprite != null) + { + DamagedSprite.Draw(spriteBatch, + new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), + color * damageOverlayStrength, activeSprite.Origin, + -body.DrawRotation, + Scale, spriteEffect, activeSprite.Depth - depthStep * Math.Max(1, WearingItems.Count * 2)); // Multiply by 2 to get rid of z-fighting with some clothing combos + } + } if (GameMain.DebugDraw) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs index eb85f6b64..795b4ae24 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs @@ -359,7 +359,7 @@ namespace Barotrauma.Transition else { //copying a mod: we have a neat method for that! - await SteamManager.Workshop.CopyDirectory(path, Path.GetFileName(path), path, destPath); + await SteamManager.Workshop.CopyDirectory(path, Path.GetFileName(path), path, destPath, SteamManager.Workshop.ShouldCorrectPaths.Yes); return null; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index a13d82f7f..c56a20796 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -125,7 +125,7 @@ namespace Barotrauma { if (isOpen) { - frame.AddToGUIUpdateList(); + frame.AddToGUIUpdateList(order: 1); } } @@ -1714,9 +1714,47 @@ namespace Barotrauma //check missing mission texts foreach (var missionPrefab in MissionPrefab.Prefabs) { - Identifier missionId = (missionPrefab.ConfigElement.GetAttribute("textidentifier") == null ? missionPrefab.Identifier : missionPrefab.ConfigElement.GetAttributeIdentifier("textidentifier", Identifier.Empty)); - addIfMissing($"missionname.{missionId}".ToIdentifier(), language); - addIfMissing($"missiondescription.{missionId}".ToIdentifier(), language); + Identifier missionId = missionPrefab.ConfigElement.GetAttribute("textidentifier") == null ? + missionPrefab.Identifier : + missionPrefab.ConfigElement.GetAttributeIdentifier("textidentifier", Identifier.Empty); + + if (!tags[language].Contains(missionPrefab.ConfigElement.GetAttributeIdentifier("name", Identifier.Empty))) + { + addIfMissing($"missionname.{missionId}".ToIdentifier(), language); + } + + if (missionPrefab.Type == MissionType.Combat) + { + addIfMissing($"MissionDescriptionNeutral.{missionId}".ToIdentifier(), language); + addIfMissing($"MissionDescription1.{missionId}".ToIdentifier(), language); + addIfMissing($"MissionDescription2.{missionId}".ToIdentifier(), language); + addIfMissing($"MissionTeam1.{missionId}".ToIdentifier(), language); + addIfMissing($"MissionTeam2.{missionId}".ToIdentifier(), language); + } + else + { + if (!tags[language].Contains(missionPrefab.ConfigElement.GetAttributeIdentifier("description", Identifier.Empty))) + { + addIfMissing($"missiondescription.{missionId}".ToIdentifier(), language); + } + if (!tags[language].Contains(missionPrefab.ConfigElement.GetAttributeIdentifier("successmessage", Identifier.Empty))) + { + addIfMissing($"missionsuccess.{missionId}".ToIdentifier(), language); + } + //only check failure message if there's something defined in the xml (otherwise we just use the generic "missionfailed" text) + if (missionPrefab.ConfigElement.GetAttribute("failuremessage") != null && + !tags[language].Contains(missionPrefab.ConfigElement.GetAttributeIdentifier("failuremessage", Identifier.Empty))) + { + addIfMissing($"missionfailure.{missionId}".ToIdentifier(), language); + } + } + for (int i = 0; i type.IsSubclassOf(typeof(ItemComponent)))) @@ -2503,7 +2541,7 @@ namespace Barotrauma var entity = MapEntity.mapEntityList[i] as ISerializableEntity; if (entity != null) { - List> allProperties = new List>(); + List<(object obj, SerializableProperty property)> allProperties = new List<(object obj, SerializableProperty property)>(); if (entity is Item item) { @@ -2518,14 +2556,14 @@ namespace Barotrauma for (int k = 0; k < properties.Count; k++) { - allProperties.Add(new Pair(entity, properties[k])); + allProperties.Add((entity, properties[k])); } } for (int j = 0; j < allProperties.Count; j++) { - var property = allProperties[j].Second; - string propertyName = (allProperties[j].First.GetType().Name + "." + property.PropertyInfo.Name).ToLowerInvariant(); + var property = allProperties[j].property; + string propertyName = (allProperties[j].obj.GetType().Name + "." + property.PropertyInfo.Name).ToLowerInvariant(); LocalizedString displayName = TextManager.Get($"sp.{propertyName}.name"); if (displayName.IsNullOrEmpty()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index e4c047bff..404c09927 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -35,7 +35,6 @@ namespace Barotrauma public TextGetterHandler TextGetter; public bool Wrap; - private bool playerInput; public bool RoundToNearestPixel = true; @@ -287,8 +286,7 @@ namespace Barotrauma /// If the rectT height is set 0, the height is calculated from the text. /// public GUITextBlock(RectTransform rectT, RichString text, Color? textColor = null, GUIFont font = null, - Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null, - bool playerInput = false) + Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null) : base(style, rectT) { if (color.HasValue) @@ -307,7 +305,6 @@ namespace Barotrauma this.textAlignment = textAlignment; this.Wrap = wrap; this.Text = text ?? ""; - this.playerInput = playerInput; if (rectT.Rect.Height == 0 && !text.IsNullOrEmpty()) { CalculateHeightFromText(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 2bcb6bbb7..d48669710 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -261,7 +261,7 @@ namespace Barotrauma this.color = color ?? Color.White; frame = new GUIFrame(new RectTransform(Vector2.One, rectT, Anchor.Center), style, color); GUIStyle.Apply(frame, style == "" ? "GUITextBox" : style); - textBlock = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform, Anchor.CenterLeft), text ?? "", textColor, font, textAlignment, wrap, playerInput: true); + textBlock = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform, Anchor.CenterLeft), text ?? "", textColor, font, textAlignment, wrap); GUIStyle.Apply(textBlock, "", this); if (font != null) { textBlock.Font = font; } CaretEnabled = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index 211f69381..a86a2ca4c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -657,6 +657,7 @@ namespace Barotrauma { CanBeFocused = false }; + GUILayoutGroup parentLayout = new GUILayoutGroup(new RectTransform(Vector2.One, backgroundFrame.RectTransform), isHorizontal: true) { Stretch = true }; if (!(affliction.Prefab is { } prefab)) { return; } @@ -676,7 +677,7 @@ namespace Barotrauma GUIFrame textContainer = new GUIFrame(new RectTransform(new Vector2(0.6f, 1f), textLayout.RectTransform), style: null); GUITextBlock afflictionName = new GUITextBlock(new RectTransform(Vector2.One, textContainer.RectTransform), name, font: GUIStyle.SubHeadingFont); - GUITextBlock healCost = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), textLayout.RectTransform), TextManager.FormatCurrency(affliction.Price), textAlignment: Alignment.Center, font: GUIStyle.LargeFont) + GUITextBlock healCost = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), textLayout.RectTransform), TextManager.FormatCurrency(affliction.Price), textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero }; @@ -746,12 +747,12 @@ namespace Barotrauma ClosePopup(); - GUIFrame mainFrame = new GUIFrame(new RectTransform(new Vector2(0.28f, 0.45f), container.RectTransform) + GUIFrame mainFrame = new GUIFrame(new RectTransform(new Vector2(0.28f, 0.5f), container.RectTransform) { ScreenSpaceOffset = location.ToPoint() }); - GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), mainFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.01f, Stretch = true }; + GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), mainFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.01f, Stretch = true }; if (mainFrame.Rect.Bottom > GameMain.GraphicsHeight) { @@ -819,7 +820,9 @@ namespace Barotrauma if (!(affliction.Prefab is { } prefab)) { return ImmutableArray.Empty; } GUIFrame backgroundFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.33f), parent.RectTransform), style: "ListBoxElement"); - GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), backgroundFrame.RectTransform, Anchor.Center)) + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), backgroundFrame.RectTransform, Anchor.BottomCenter), style: "HorizontalLine"); + + GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), backgroundFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.05f }; @@ -862,13 +865,27 @@ namespace Barotrauma GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.66f), mainLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - GUILayoutGroup bottomTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 1f), bottomLayout.RectTransform)); - GUITextBlock descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), bottomTextLayout.RectTransform), ToolBox.LimitString(prefab.Description, GUIStyle.Font, GUI.IntScale(64)), wrap: true) + GUILayoutGroup bottomTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 1f), bottomLayout.RectTransform)) + { + RelativeSpacing = 0.05f + }; + GUITextBlock descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.6f), bottomTextLayout.RectTransform), prefab.Description, font: GUIStyle.SmallFont, wrap: true) { ToolTip = prefab.Description }; + bool truncated = false; + while (descriptionBlock.TextSize.Y > descriptionBlock.Rect.Height && descriptionBlock.WrappedText.Contains('\n')) + { + var split = descriptionBlock.WrappedText.Value.Split('\n'); + descriptionBlock.Text = string.Join('\n', split.Take(split.Length - 1)); + truncated = true; + } + if (truncated) + { + descriptionBlock.Text += "..."; + } - GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), bottomTextLayout.RectTransform), TextManager.FormatCurrency(affliction.Price), font: GUIStyle.LargeFont); + GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.25f), bottomTextLayout.RectTransform), TextManager.FormatCurrency(affliction.Price), font: GUIStyle.SubHeadingFont); GUIButton buyButton = new GUIButton(new RectTransform(new Vector2(0.2f, 0.75f), bottomLayout.RectTransform), style: "CrewManagementAddButton"); @@ -923,6 +940,7 @@ namespace Barotrauma }); } + #warning TODO: this doesn't seem like the right place for this, and it's not clear from the method signature how this differs from ToolBox.LimitString public static void EnsureTextDoesntOverflow(string? text, GUITextBlock textBlock, Rectangle bounds, ImmutableArray? layoutGroups = null) { if (string.IsNullOrWhiteSpace(text)) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index e51c586c9..303a207fd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -46,9 +46,7 @@ namespace Barotrauma private int buyTotal, sellTotal, sellFromSubTotal; private GUITextBlock storeNameBlock; - private GUITextBlock merchantBalanceBlock; - private GUITextBlock currentSellValueBlock, newSellValueBlock; - private GUIImage sellValueChangeArrow; + private GUITextBlock reputationEffectBlock; private GUIDropDown sortingDropDown; private GUITextBox searchBox; private GUILayoutGroup categoryButtonContainer; @@ -376,41 +374,29 @@ namespace Barotrauma AutoScaleVertical = true, ForceUpperCase = ForceUpperCase.Yes }; - merchantBalanceBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), merchantBalanceContainer.RectTransform), - "", font: GUIStyle.SubHeadingFont) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), merchantBalanceContainer.RectTransform), "", + color: Color.White, font: GUIStyle.SubHeadingFont) { AutoScaleVertical = true, TextScale = 1.1f, - TextGetter = () => - { - merchantBalanceBlock.TextColor = ActiveStore?.BalanceColor ?? Color.Red; - return GetMerchantBalanceText(); - } + TextGetter = () => GetMerchantBalanceText() }; // Item sell value ------------------------------------------------ - var sellValueContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), balanceAndValueGroup.RectTransform)) + var reputationEffectContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), balanceAndValueGroup.RectTransform)) { CanBeFocused = true, - RelativeSpacing = 0.005f + RelativeSpacing = 0.005f, + ToolTip = TextManager.Get("campaignstore.reputationtooltip") }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), sellValueContainer.RectTransform), - TextManager.Get("campaignstore.sellvalue"), font: GUIStyle.Font, textAlignment: Alignment.BottomLeft) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), reputationEffectContainer.RectTransform), + TextManager.Get("reputation"), font: GUIStyle.Font, textAlignment: Alignment.BottomLeft) { AutoScaleVertical = true, CanBeFocused = false, - ForceUpperCase = ForceUpperCase.Yes + ForceUpperCase = ForceUpperCase.Yes, }; - - var valueChangeGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), sellValueContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - CanBeFocused = false, - RelativeSpacing = 0.02f - }; - float blockWidth = GUI.IsFourByThree() ? 0.32f : 0.28f; - Point blockMaxSize = new Point((int)(GameSettings.CurrentConfig.Graphics.TextScale * 60), valueChangeGroup.Rect.Height); - currentSellValueBlock = new GUITextBlock(new RectTransform(new Vector2(blockWidth, 1.0f), valueChangeGroup.RectTransform) { MaxSize = blockMaxSize }, - "", font: GUIStyle.SubHeadingFont) + reputationEffectBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), reputationEffectContainer.RectTransform), "", font: GUIStyle.SubHeadingFont) { AutoScaleVertical = true, CanBeFocused = false, @@ -419,64 +405,27 @@ namespace Barotrauma { if (CurrentLocation != null) { - int balanceAfterTransaction = activeTab switch + Color textColor = GUIStyle.ColorReputationNeutral; + string sign = ""; + int reputationModifier = (int)MathF.Round((CurrentLocation.GetStoreReputationModifier(activeTab == StoreTab.Buy) - 1) * 100); + if (reputationModifier > 0) { - StoreTab.Buy => ActiveStore.Balance + buyTotal, - StoreTab.Sell => ActiveStore.Balance - sellTotal, - StoreTab.SellSub => ActiveStore.Balance - sellFromSubTotal, - _ => throw new NotImplementedException(), - }; - if (balanceAfterTransaction != ActiveStore.Balance) - { - var newStatus = CurrentLocation.GetStoreBalanceStatus(balanceAfterTransaction); - if (ActiveStore.ActiveBalanceStatus.SellPriceModifier != newStatus.SellPriceModifier) - { - string tooltipTag = newStatus.SellPriceModifier > ActiveStore.ActiveBalanceStatus.SellPriceModifier ? - "campaingstore.valueincreasetooltip" : "campaingstore.valuedecreasetooltip"; - sellValueContainer.ToolTip = TextManager.Get(tooltipTag); - currentSellValueBlock.TextColor = newStatus.Color; - sellValueChangeArrow.Color = newStatus.Color; - sellValueChangeArrow.Visible = true; - newSellValueBlock.TextColor = newStatus.Color; - newSellValueBlock.Text = $"{(newStatus.SellPriceModifier * 100).FormatZeroDecimal()} %"; - return $"{(ActiveStore.ActiveBalanceStatus.SellPriceModifier * 100).FormatZeroDecimal()} %"; - } + textColor = IsBuying ? GUIStyle.ColorReputationLow : GUIStyle.ColorReputationHigh; + sign = "+"; } - sellValueContainer.ToolTip = TextManager.Get("campaignstore.sellvaluetooltip"); - currentSellValueBlock.TextColor = ActiveStore.BalanceColor; - sellValueChangeArrow.Visible = false; - newSellValueBlock.Text = null; - return $"{(ActiveStore.ActiveBalanceStatus.SellPriceModifier * 100).FormatZeroDecimal()} %"; + else if (reputationModifier < 0) + { + textColor = IsBuying ? GUIStyle.ColorReputationHigh : GUIStyle.ColorReputationLow; + } + reputationEffectBlock.TextColor = textColor; + return $"{sign}{reputationModifier}%"; } else { - sellValueContainer.ToolTip = null; - sellValueChangeArrow.Visible = false; - newSellValueBlock.Text = null; - return null; + return ""; } } }; - Vector4 newPadding = currentSellValueBlock.Padding; - newPadding.Z = 0; - currentSellValueBlock.Padding = newPadding; - float relativeHeight = 0.45f; - float relativeWidth = (relativeHeight * valueChangeGroup.Rect.Height) / valueChangeGroup.Rect.Width; - sellValueChangeArrow = new GUIImage(new RectTransform(new Vector2(relativeWidth, relativeHeight), valueChangeGroup.RectTransform), "StoreArrow", scaleToFit: true) - { - CanBeFocused = false, - Visible = false - }; - newSellValueBlock = new GUITextBlock(new RectTransform(new Vector2(blockWidth, 1.0f), valueChangeGroup.RectTransform) { MaxSize = blockMaxSize }, - "", font: GUIStyle.SubHeadingFont) - { - AutoScaleVertical = true, - CanBeFocused = false, - TextScale = 1.1f - }; - newPadding = newSellValueBlock.Padding; - newPadding.X = 0; - newSellValueBlock.Padding = newPadding; // Store mode buttons ------------------------------------------------ var modeButtonFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.4f / 14.0f), storeContent.RectTransform), style: null); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 35ec743ca..707e3580a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -626,7 +626,7 @@ namespace Barotrauma { if (GameMain.Client == null) { - SubmarineInfo newSub = GameMain.GameSession.SwitchSubmarine(selectedSubmarine, deliveryFee); + GameMain.GameSession.SwitchSubmarine(selectedSubmarine, deliveryFee); RefreshSubmarineDisplay(true); } else @@ -664,7 +664,7 @@ namespace Barotrauma if (GameMain.Client == null) { GameMain.GameSession.PurchaseSubmarine(selectedSubmarine); - SubmarineInfo newSub = GameMain.GameSession.SwitchSubmarine(selectedSubmarine, 0); + GameMain.GameSession.SwitchSubmarine(selectedSubmarine, 0); RefreshSubmarineDisplay(true); } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 8530291a5..87b4b2e92 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -54,47 +54,65 @@ namespace Barotrauma private ushort currentPing; private readonly Character character; - private readonly bool hasCharacter; + private readonly bool wasCharacterAlive; private readonly GUITextBlock textBlock; private readonly GUIFrame frame; private readonly GUIImage permissionIcon; - public LinkedGUI(Client client, GUIFrame frame, bool hasCharacter, GUITextBlock textBlock, GUIImage permissionIcon) + public LinkedGUI(Client client, GUIFrame frame, GUITextBlock textBlock, GUIImage permissionIcon) { this.Client = client; this.textBlock = textBlock; this.frame = frame; - this.hasCharacter = hasCharacter; this.permissionIcon = permissionIcon; + character = client?.Character; + wasCharacterAlive = client.Character != null && !client.Character.IsDead; } - public LinkedGUI(Character character, GUIFrame frame, bool hasCharacter, GUITextBlock textBlock) + public LinkedGUI(Character character, GUIFrame frame, GUITextBlock textBlock) { this.character = character; this.textBlock = textBlock; this.frame = frame; - this.hasCharacter = hasCharacter; + wasCharacterAlive = character != null && !character.IsDead; } public bool HasMultiplayerCharacterChanged() { if (Client == null) { return false; } - bool characterState = Client.Character != null; - if (characterState && Client.Character.IsDead) characterState = false; - return hasCharacter != characterState; + + if (GameSettings.CurrentConfig.VerboseLogging) + { + if (Client.Character != character) + { + DebugConsole.Log($"Refreshing tab menu crew list (client \"{Client.Name}\"'s character changed from \"{character?.Name ?? "null"}\" to \"{Client.Character?.Name ?? "null"}\")"); + } + } + return Client.Character != character; } - public bool HasMultiplayerCharacterDied() - { - if (Client == null || !hasCharacter || Client.Character == null) { return false; } - return Client.Character.IsDead; - } - - public bool HasAICharacterDied() + public bool HasCharacterDied() { if (character == null) { return false; } - return character.IsDead; + bool isAlive = !(character?.IsDead ?? true); + if (GameSettings.CurrentConfig.VerboseLogging) + { + if (wasCharacterAlive && !isAlive) + { + DebugConsole.Log(Client == null ? + $"Refreshing tab menu crew list (character \"{character?.Name ?? "null"}\" died)" : + $"Refreshing tab menu crew list (client \"{Client.Name}\"'s character \"{character?.Name ?? "null"}\" died)"); + } + else if (!wasCharacterAlive && isAlive) + { + DebugConsole.Log(Client == null ? + + $"Refreshing tab menu crew list (character \"{character?.Name ?? "null"}\" came back to life)" : + $"Refreshing tab menu crew list (client \"{Client.Name}\"'s character \"{character?.Name ?? "null"}\" came back to life)"); + } + } + return isAlive != wasCharacterAlive; } public void TryPingRefresh() @@ -207,7 +225,7 @@ namespace Barotrauma { linkedGUIList[i].TryPingRefresh(); linkedGUIList[i].TryPermissionIconRefresh(GetPermissionIcon(linkedGUIList[i].Client)); - if (linkedGUIList[i].HasMultiplayerCharacterChanged() || linkedGUIList[i].HasMultiplayerCharacterDied() || linkedGUIList[i].HasAICharacterDied()) + if (linkedGUIList[i].HasMultiplayerCharacterChanged() || linkedGUIList[i].HasCharacterDied()) { RemoveCurrentElements(); CreateMultiPlayerList(true); @@ -219,10 +237,11 @@ namespace Barotrauma { for (int i = 0; i < linkedGUIList.Count; i++) { - if (linkedGUIList[i].HasAICharacterDied()) + if (linkedGUIList[i].HasCharacterDied()) { RemoveCurrentElements(); CreateSinglePlayerList(true); + return; } } } @@ -297,6 +316,10 @@ namespace Barotrauma var balanceFrame = new GUIFrame(new RectTransform(new Point(innerLayoutGroup.Rect.Width, innerLayoutGroup.Rect.Height - infoFrameHolderHeight), parent: innerLayoutGroup.RectTransform), style: "InnerFrame"); GUITextBlock balanceText = new GUITextBlock(new RectTransform(Vector2.One, balanceFrame.RectTransform), string.Empty, textAlignment: Alignment.Right); + if (GameMain.IsMultiplayer) + { + balanceText.ToolTip = TextManager.Get("bankdescription"); + } GUIFrame bottomDisclaimerFrame = new GUIFrame(new RectTransform(new Vector2(contentFrameSize.X, 0.1f), infoFrame.RectTransform) { AbsoluteOffset = new Point(contentFrame.Rect.X, contentFrame.Rect.Bottom + GUI.IntScale(8)) @@ -337,7 +360,7 @@ namespace Barotrauma var talentsButton = createTabButton(InfoFrameTab.Talents, "tabmenu.character"); talentsButton.OnAddedToGUIUpdateList += (component) => { - talentsButton.Enabled = Character.Controlled?.Info != null; + talentsButton.Enabled = Character.Controlled?.Info != null || (GameMain.Client?.CharacterInfo != null && GameMain.GameSession?.GameMode is MultiPlayerCampaign); if (!talentsButton.Enabled && selectedTab == InfoFrameTab.Talents) { SelectInfoFrameTab(InfoFrameTab.Crew); @@ -560,7 +583,7 @@ namespace Barotrauma GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), ToolBox.LimitString(character.Info.Name, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: character.Info.Job.Prefab.UIColor); - linkedGUIList.Add(new LinkedGUI(character, frame, !character.IsDead, textBlock: null)); + linkedGUIList.Add(new LinkedGUI(character, frame, textBlock: null)); } private void CreateMultiPlayerListContentHolder(GUILayoutGroup headerFrame) @@ -657,7 +680,7 @@ namespace Barotrauma if (client != null) { CreateNameWithPermissionIcon(client, paddedFrame, out GUIImage permissionIcon); - linkedGUIList.Add(new LinkedGUI(client, frame, true, + linkedGUIList.Add(new LinkedGUI(client, frame, new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), client.Ping.ToString(), textAlignment: Alignment.Center), permissionIcon)); } @@ -668,12 +691,12 @@ namespace Barotrauma if (character is AICharacter) { - linkedGUIList.Add(new LinkedGUI(character, frame, !character.IsDead, + linkedGUIList.Add(new LinkedGUI(character, frame, new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), TextManager.Get("tabmenu.bot"), textAlignment: Alignment.Center) { ForceUpperCase = ForceUpperCase.Yes })); } else { - linkedGUIList.Add(new LinkedGUI(client: null, frame, true, textBlock: null, permissionIcon: null)); + linkedGUIList.Add(new LinkedGUI(client: null, frame, textBlock: null, permissionIcon: null)); new GUICustomComponent(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => DrawDisconnectedIcon(sb, component.Rect)) { @@ -718,7 +741,7 @@ namespace Barotrauma }; CreateNameWithPermissionIcon(client, paddedFrame, out GUIImage permissionIcon); - linkedGUIList.Add(new LinkedGUI(client, frame, false, + linkedGUIList.Add(new LinkedGUI(client, frame, new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), client.Ping.ToString(), textAlignment: Alignment.Center), permissionIcon)); @@ -775,19 +798,27 @@ namespace Barotrauma Stretch = true }; + new GUIFrame(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform), style: null) + { + IgnoreLayoutGroups = true, + ToolTip = TextManager.Get("walletdescription") + }; + if (character.IsBot) { return; } Sprite walletSprite = GUIStyle.CrewWalletIconSmall.Value.Sprite; - GUIImage icon = new GUIImage(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform, scaleBasis: ScaleBasis.BothHeight), walletSprite, scaleToFit: true); + GUIImage icon = new GUIImage(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform, scaleBasis: ScaleBasis.BothHeight), walletSprite, scaleToFit: true) { CanBeFocused = false }; GUITextBlock walletBlock = new GUITextBlock(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform), string.Empty, textAlignment: Alignment.Right, font: GUIStyle.Font) { AutoScaleHorizontal = true, - Padding = Vector4.Zero + Padding = Vector4.Zero, + CanBeFocused = false }; GUIImage largeIcon = new GUIImage(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform), walletSprite, scaleToFit: true) { + CanBeFocused = false, IgnoreLayoutGroups = true, Visible = false }; @@ -971,16 +1002,25 @@ namespace Barotrauma float relativeX = icon.RectTransform.NonScaledSize.X / (float)icon.Parent.RectTransform.NonScaledSize.X; GUILayoutGroup headerTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - relativeX, 1f), headerLayout.RectTransform), isHorizontal: true) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), headerTextLayout.RectTransform), TextManager.Get("crewwallet.wallet"), font: GUIStyle.LargeFont); + GUIFrame walletTooltipFrame = new GUIFrame(new RectTransform(Vector2.One, headerLayout.RectTransform), style: null) + { + IgnoreLayoutGroups = true, + ToolTip = TextManager.Get("walletdescription") + }; GUITextBlock moneyBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), headerTextLayout.RectTransform), TextManager.FormatCurrency(targetWallet.Balance), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right); GUILayoutGroup middleLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.66f), walletLayout.RectTransform)); GUILayoutGroup salaryTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), middleLayout.RectTransform), isHorizontal: true); GUITextBlock salaryTitle = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), salaryTextLayout.RectTransform), TextManager.Get("crewwallet.salary"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); GUITextBlock rewardBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), salaryTextLayout.RectTransform), string.Empty, textAlignment: Alignment.BottomRight); + GUIFrame salaryTooltipFrame = new GUIFrame(new RectTransform(Vector2.One, middleLayout.RectTransform), style: null) + { + IgnoreLayoutGroups = true, + ToolTip = TextManager.Get("crewwallet.salary.tooltip") + }; GUILayoutGroup sliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), middleLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.Center); GUIScrollBar salarySlider = new GUIScrollBar(new RectTransform(new Vector2(0.9f, 1f), sliderLayout.RectTransform), style: "GUISlider", barSize: 0.03f) { - ToolTip = TextManager.Get("crewwallet.salary.tooltip"), Range = new Vector2(0, 1), BarScrollValue = targetWallet.RewardDistribution / 100f, Step = 0.01f, @@ -1050,149 +1090,195 @@ namespace Barotrauma return true; } }; + + Identifier eventIdentifier = nameof(CreateWalletFrame).ToIdentifier(); + ToggleTransferMenuIcon(transferMenuButton, open: isTransferMenuOpen); ToggleCenterButton(centerButton, isSending); - - if (!(Character.Controlled is { } myCharacter)) - { - salarySlider.Enabled = false; - transferAmountInput.Enabled = false; - centerButton.Enabled = false; - confirmButton.Enabled = false; - return; - } - - bool hasMoneyPermissions = CampaignMode.AllowedToManageWallets(); - salarySlider.Enabled = hasMoneyPermissions; Wallet otherWallet; + GameMain.Client?.OnPermissionChanged.RegisterOverwriteExisting(eventIdentifier, e => UpdateWalletInterface(registerEvents: false)); + UpdateWalletInterface(registerEvents: true); - switch (hasMoneyPermissions) + void UpdateWalletInterface(bool registerEvents) { - case true: - rightName.Text = TextManager.Get("crewwallet.bank"); - otherWallet = campaign.Bank; - break; - case false when character == myCharacter: - rightName.Text = TextManager.Get("crewwallet.bank"); - otherWallet = campaign.Bank; - isSending = true; - ToggleCenterButton(centerButton, isSending); - break; - default: - rightName.Text = myCharacter.Name; - otherWallet = campaign.PersonalWallet; - break; - } - - MedicalClinicUI.EnsureTextDoesntOverflow(rightName.Text.ToString(), rightName, rightLayout.Rect, layoutGroups); - updateButtonText(); - if (!hasMoneyPermissions) - { - if (character != Character.Controlled) + if (!(Character.Controlled is { } myCharacter)) { - centerButton.Enabled = centerButton.CanBeFocused = false; + salarySlider.Enabled = false; + transferAmountInput.Enabled = false; + centerButton.Enabled = false; + confirmButton.Enabled = false; + return; } - salarySlider.Enabled = salarySlider.CanBeFocused = false; - } - leftBalance.Text = TextManager.FormatCurrency(otherWallet.Balance); + bool hasMoneyPermissions = CampaignMode.AllowedToManageWallets(); + salarySlider.Enabled = hasMoneyPermissions; - UpdateAllInputs(); + switch (hasMoneyPermissions) + { + case true: + rightName.Text = TextManager.Get("crewwallet.bank"); + otherWallet = campaign.Bank; + break; + case false when character == myCharacter: + rightName.Text = TextManager.Get("crewwallet.bank"); + otherWallet = campaign.Bank; + isSending = true; + ToggleCenterButton(centerButton, isSending); + break; + default: + rightName.Text = myCharacter.Name; + otherWallet = campaign.PersonalWallet; + break; + } + + MedicalClinicUI.EnsureTextDoesntOverflow(rightName.Text.ToString(), rightName, rightLayout.Rect, layoutGroups); + + UpdatedConfirmButtonText(); + + if (!hasMoneyPermissions) + { + if (character != Character.Controlled) + { + centerButton.Enabled = centerButton.CanBeFocused = false; + } + + salarySlider.Enabled = salarySlider.CanBeFocused = false; + } + + leftBalance.Text = TextManager.FormatCurrency(otherWallet.Balance); - centerButton.OnClicked = (btn, o) => - { - isSending = !isSending; - updateButtonText(); - ToggleCenterButton(btn, isSending); UpdateAllInputs(); - return true; - }; - void updateButtonText() - { - confirmButton.Text = TextManager.Get(hasMoneyPermissions || isSending ? "confirm" : "crewwallet.requestmoney"); - } + if (!registerEvents) { return; } - transferAmountInput.OnValueChanged = input => - { - UpdateInputs(); - }; - - transferAmountInput.OnValueEntered = input => - { - UpdateAllInputs(); - }; - - Identifier eventIdentifier = nameof(CreateWalletFrame).ToIdentifier(); - campaign.OnMoneyChanged.RegisterOverwriteExisting(eventIdentifier, e => - { - if (e.Wallet == targetWallet) + centerButton.OnClicked = (btn, o) => { - moneyBlock.Text = TextManager.FormatCurrency(e.Info.Balance); - salarySlider.BarScrollValue = e.Info.RewardDistribution / 100f; + isSending = !isSending; + UpdatedConfirmButtonText(); + ToggleCenterButton(btn, isSending); + UpdateAllInputs(); + return true; + }; + + transferAmountInput.OnValueChanged = input => + { + UpdateInputs(); + }; + + transferAmountInput.OnValueEntered = input => + { + UpdateAllInputs(); + }; + + resetButton.OnClicked = (button, o) => + { + transferAmountInput.IntValue = 0; + UpdateAllInputs(); + return true; + }; + + confirmButton.OnClicked = (button, o) => + { + int amount = transferAmountInput.IntValue; + if (amount == 0) { return false; } + + Option target1 = Option.Some(character), + target2 = otherWallet == campaign.Bank ? Option.None() : Option.Some(myCharacter); + if (isSending) { (target1, target2) = (target2, target1); } + + SendTransaction(target1, target2, amount); + isTransferMenuOpen = false; + ToggleTransferMenuIcon(transferMenuButton, isTransferMenuOpen); + return true; + }; + + campaign.OnMoneyChanged.RegisterOverwriteExisting(eventIdentifier, e => + { + if (e.Wallet == targetWallet) + { + moneyBlock.Text = TextManager.FormatCurrency(e.Info.Balance); + salarySlider.BarScrollValue = e.Info.RewardDistribution / 100f; + } + + UpdateAllInputs(); + }); + + registeredEvents.Add(eventIdentifier); + + void UpdatedConfirmButtonText() + { + confirmButton.Text = TextManager.Get(hasMoneyPermissions || isSending ? "confirm" : "crewwallet.requestmoney"); } - UpdateAllInputs(); - }); - registeredEvents.Add(eventIdentifier); - resetButton.OnClicked = (button, o) => - { - transferAmountInput.IntValue = 0; - UpdateAllInputs(); - return true; - }; - - confirmButton.OnClicked = (button, o) => - { - int amount = transferAmountInput.IntValue; - if (amount == 0) { return false; } - - Option target1 = Option.Some(character), - target2 = otherWallet == campaign.Bank ? Option.None() : Option.Some(myCharacter); - if (isSending) { (target1, target2) = (target2, target1); } - - SendTransaction(target1, target2, amount); - isTransferMenuOpen = false; - ToggleTransferMenuIcon(transferMenuButton, isTransferMenuOpen); - return true; - }; - - void UpdateAllInputs() - { - UpdateInputs(); - UpdateMaxInput(); - } - - void UpdateInputs() - { - confirmButton.Enabled = resetButton.Enabled = transferAmountInput.IntValue > 0; - if (transferAmountInput.IntValue == 0) + void UpdateAllInputs() { - rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance); - rightBalance.TextColor = GUIStyle.TextColorNormal; - leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance); - leftBalance.TextColor = GUIStyle.TextColorNormal; + UpdateInputs(); + UpdateMaxInput(); } - else if (isSending) + + void UpdateInputs() { - rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance + transferAmountInput.IntValue); - rightBalance.TextColor = GUIStyle.Blue; - leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance - transferAmountInput.IntValue); - leftBalance.TextColor = GUIStyle.Red; + confirmButton.Enabled = resetButton.Enabled = transferAmountInput.IntValue > 0; + if (transferAmountInput.IntValue == 0) + { + rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance); + rightBalance.TextColor = GUIStyle.TextColorNormal; + leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance); + leftBalance.TextColor = GUIStyle.TextColorNormal; + } + else if (isSending) + { + rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance + transferAmountInput.IntValue); + rightBalance.TextColor = GUIStyle.Blue; + leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance - transferAmountInput.IntValue); + leftBalance.TextColor = GUIStyle.Red; + } + else + { + rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance - transferAmountInput.IntValue); + rightBalance.TextColor = GUIStyle.Red; + leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance + transferAmountInput.IntValue); + leftBalance.TextColor = GUIStyle.Blue; + } } - else + + void UpdateMaxInput() { - rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance - transferAmountInput.IntValue); - rightBalance.TextColor = GUIStyle.Red; - leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance + transferAmountInput.IntValue); - leftBalance.TextColor = GUIStyle.Blue; + int maxValue = isSending ? targetWallet.Balance : otherWallet.Balance; + transferAmountInput.MaxValueInt = maxValue; + + transferAmountInput.Enabled = true; + transferAmountInput.ToolTip = string.Empty; + + if (!hasMoneyPermissions && GameMain.Client?.ServerSettings is { } serverSettings) + { + transferAmountInput.MaxValueInt = Math.Min(maxValue, serverSettings.MaximumTransferRequest); + if (serverSettings.MaximumTransferRequest <= 0) + { + transferAmountInput.Enabled = false; + transferAmountInput.ToolTip = TextManager.Get("wallettransferrequestdisabled"); + } + } } } - void UpdateMaxInput() + void SetRewardText(int value, GUITextBlock block) { - transferAmountInput.MaxValueInt = isSending ? targetWallet.Balance : otherWallet.Balance; + var (_, percentage, sum) = Mission.GetRewardShare(value, salaryCrew, Option.None()); + LocalizedString tooltip = string.Empty; + block.TextColor = GUIStyle.TextColorNormal; + + if (sum > 100) + { + tooltip = TextManager.GetWithVariables("crewwallet.salary.over100toolitp", ("[sum]", $"{(int)sum}"), ("[newvalue]", $"{percentage}")); + block.TextColor = GUIStyle.Orange; + } + + LocalizedString text = TextManager.GetWithVariable("percentageformat", "[value]", $"{value}"); + + block.Text = text; + block.ToolTip = RichString.Rich(tooltip); } static void ToggleTransferMenuIcon(GUIButton btn, bool open) @@ -1235,24 +1321,6 @@ namespace Barotrauma transfer.Write(msg); GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); } - - void SetRewardText(int value, GUITextBlock block) - { - var (_, percentage, sum) = Mission.GetRewardShare(value, salaryCrew, Option.None()); - LocalizedString tooltip = string.Empty; - block.TextColor = GUIStyle.TextColorNormal; - - if (sum > 100) - { - tooltip = TextManager.GetWithVariables("crewwallet.salary.over100toolitp", ("[sum]", $"{(int)sum}"), ("[newvalue]", $"{percentage}")); - block.TextColor = GUIStyle.Orange; - } - - LocalizedString text = TextManager.GetWithVariable("percentageformat", "[value]", $"{value}"); - - block.Text = text; - block.ToolTip = RichString.Rich(tooltip); - } } private GUIComponent CreateClientInfoFrame(GUIFrame frame, Client client, Sprite permissionIcon = null) @@ -1740,9 +1808,6 @@ namespace Barotrauma talentButtons.Clear(); talentCornerIcons.Clear(); - Character controlledCharacter = Character.Controlled; - if (controlledCharacter == null) { return; } - GUIFrame talentFrameBackground = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); int padding = GUI.IntScale(15); GUIFrame talentFrameContent = new GUIFrame(new RectTransform(new Point(talentFrameBackground.Rect.Width - padding, talentFrameBackground.Rect.Height - padding), infoFrame.RectTransform, Anchor.Center), style: null); @@ -1762,13 +1827,20 @@ namespace Barotrauma GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false); } + /*Character controlledCharacter = Character.Controlled; + if (controlledCharacter == null) { return; } + if (controlledCharacter.Info is null) { DebugConsole.ThrowError("No character info found for talent UI"); return; - } + }*/ - selectedTalents = controlledCharacter.Info.GetUnlockedTalentsInTree().ToList(); + Character controlledCharacter = Character.Controlled; + CharacterInfo info = controlledCharacter?.Info ?? GameMain.Client?.CharacterInfo; + if (info == null) { return; } + + Job job = info.Job; GUILayoutGroup talentFrameLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), talentFrameMain.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter) { @@ -1776,9 +1848,7 @@ namespace Barotrauma }; GUILayoutGroup talentInfoLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), talentFrameLayoutGroup.RectTransform, Anchor.Center), isHorizontal: true); - - CharacterInfo info = controlledCharacter.Info; - Job job = info.Job; + new GUICustomComponent(new RectTransform(new Vector2(0.25f, 1f), talentInfoLayoutGroup.RectTransform), onDraw: (batch, component) => { @@ -1801,11 +1871,11 @@ namespace Barotrauma GUITextBlock traitBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), traitString, font: GUIStyle.SmallFont); traitBlock.RectTransform.NonScaledSize = traitSize.Pad(traitBlock.Padding).ToPoint(); - GUIFrame endocrineFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.35f), nameLayout.RectTransform, Anchor.BottomCenter), style: null); + GUIFrame talentsOutsideTreeFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.35f), nameLayout.RectTransform, Anchor.BottomCenter), style: null); if (!(GameMain.NetworkMember is null)) { - GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.675f, 1f), endocrineFrame.RectTransform, Anchor.TopLeft), text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew")) + GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.675f, 1f), talentsOutsideTreeFrame.RectTransform, Anchor.TopLeft), text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew")) { IgnoreLayoutGroups = true }; @@ -1852,13 +1922,14 @@ namespace Barotrauma } } - IEnumerable endocrineTalents = info.GetEndocrineTalents().Select(e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier == e)); + IEnumerable talentsOutsideTree = info.GetUnlockedTalentsOutsideTree().Select(e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier == e)); - if (endocrineTalents.Count() > 0) + if (talentsOutsideTree.Count() > 0) { - GUIImage endocrineIcon = new GUIImage(new RectTransform(new Vector2(0.275f, 1f), endocrineFrame.RectTransform, anchor: Anchor.TopRight, scaleBasis: ScaleBasis.Normal), style: "EndocrineReminderIcon") + //TODO: replace with something more generic + GUIImage endocrineIcon = new GUIImage(new RectTransform(new Vector2(0.275f, 1f), talentsOutsideTreeFrame.RectTransform, anchor: Anchor.TopRight, scaleBasis: ScaleBasis.Normal), style: "EndocrineReminderIcon") { - ToolTip = $"{TextManager.Get("afflictionname.endocrineboost")}\n\n{string.Join(", ", endocrineTalents.Select(e => e.DisplayName))}" + ToolTip = $"{TextManager.Get("afflictionname.endocrineboost")}\n\n{string.Join(", ", talentsOutsideTree.Select(e => e.DisplayName))}" }; } @@ -1870,49 +1941,55 @@ namespace Barotrauma skillBlock.RectTransform.NonScaledSize = skillSize.Pad(skillBlock.Padding).ToPoint(); skillListBox = new GUIListBox(new RectTransform(new Vector2(1f, 1f - skillBlock.RectTransform.RelativeSize.Y), skillLayout.RectTransform), style: null); - CreateTalentSkillList(controlledCharacter, skillListBox); + CreateTalentSkillList(controlledCharacter, info, skillListBox); - if (!TalentTree.JobTalentTrees.TryGet(controlledCharacter.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } - - new GUIFrame(new RectTransform(new Vector2(1f, 1f), talentFrameLayoutGroup.RectTransform), style: "HorizontalLine"); - - GUIListBox talentTreeListBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.7f), talentFrameLayoutGroup.RectTransform, Anchor.TopCenter), isHorizontal: true, style: null); - - List subTreeNames = new List(); - foreach (var subTree in talentTree.TalentSubTrees) + if (controlledCharacter != null) { - GUIFrame subTreeFrame = new GUIFrame(new RectTransform(new Vector2(0.333f, 1f), talentTreeListBox.Content.RectTransform, anchor: Anchor.TopLeft), style: null); - GUILayoutGroup subTreeLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1f), subTreeFrame.RectTransform, Anchor.Center), false, childAnchor: Anchor.TopCenter); + if (!TalentTree.JobTalentTrees.TryGet(info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } - GUIFrame subtreeTitleFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.111f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter), style: null); - int elementPadding = GUI.IntScale(8); - Point headerSize = subtreeTitleFrame.RectTransform.NonScaledSize; - GUIFrame subTreeTitleBackground = new GUIFrame(new RectTransform(new Point(headerSize.X - elementPadding, headerSize.Y), subtreeTitleFrame.RectTransform, anchor: Anchor.Center), style: "SubtreeHeader"); - subTreeNames.Add(new GUITextBlock(new RectTransform(Vector2.One, subTreeTitleBackground.RectTransform, anchor: Anchor.TopCenter), subTree.DisplayName, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center)); + new GUIFrame(new RectTransform(new Vector2(1f, 1f), talentFrameLayoutGroup.RectTransform), style: "HorizontalLine"); - for (int i = 0; i < 4; i++) + GUIListBox talentTreeListBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.7f), talentFrameLayoutGroup.RectTransform, Anchor.TopCenter), isHorizontal: true, style: null); + + selectedTalents = info.GetUnlockedTalentsInTree().ToList(); + + List subTreeNames = new List(); + foreach (var subTree in talentTree.TalentSubTrees) { - GUIFrame talentOptionFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.222f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter), style: null); + GUIFrame subTreeFrame = new GUIFrame(new RectTransform(new Vector2(0.333f, 1f), talentTreeListBox.Content.RectTransform, anchor: Anchor.TopLeft), style: null); + GUILayoutGroup subTreeLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1f), subTreeFrame.RectTransform, Anchor.Center), false, childAnchor: Anchor.TopCenter); - Point talentFrameSize = talentOptionFrame.RectTransform.NonScaledSize; + GUIFrame subtreeTitleFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.111f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter), style: null); + int elementPadding = GUI.IntScale(8); + Point headerSize = subtreeTitleFrame.RectTransform.NonScaledSize; + GUIFrame subTreeTitleBackground = new GUIFrame(new RectTransform(new Point(headerSize.X - elementPadding, headerSize.Y), subtreeTitleFrame.RectTransform, anchor: Anchor.Center), style: "SubtreeHeader"); + subTreeNames.Add(new GUITextBlock(new RectTransform(Vector2.One, subTreeTitleBackground.RectTransform, anchor: Anchor.TopCenter), subTree.DisplayName, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center)); - GUIFrame talentBackground = new GUIFrame(new RectTransform(new Point(talentFrameSize.X - elementPadding, talentFrameSize.Y - elementPadding), talentOptionFrame.RectTransform, anchor: Anchor.Center), style: "TalentBackground"); - GUIFrame talentBackgroundHighlight = new GUIFrame(new RectTransform(Vector2.One, talentBackground.RectTransform, anchor: Anchor.Center), style: "TalentBackgroundGlow") { Visible = false }; - - GUIImage cornerIcon = new GUIImage(new RectTransform(new Vector2(0.2f), talentOptionFrame.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight) { MaxSize = new Point(16) }, style: null) + for (int i = 0; i < 4; i++) { - CanBeFocused = false - }; + GUIFrame talentOptionFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.222f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter), style: null); - Point iconSize = cornerIcon.RectTransform.NonScaledSize; - cornerIcon.RectTransform.AbsoluteOffset = new Point(iconSize.X / 2, iconSize.Y / 2); + Point talentFrameSize = talentOptionFrame.RectTransform.NonScaledSize; + + GUIFrame talentBackground = new GUIFrame(new RectTransform(new Point(talentFrameSize.X - elementPadding, talentFrameSize.Y - elementPadding), talentOptionFrame.RectTransform, anchor: Anchor.Center), style: "TalentBackground") + { + Color = talentStageBackgroundColors[TalentTree.TalentTreeStageState.Locked] + }; + GUIFrame talentBackgroundHighlight = new GUIFrame(new RectTransform(Vector2.One, talentBackground.RectTransform, anchor: Anchor.Center), style: "TalentBackgroundGlow") { Visible = false }; + + GUIImage cornerIcon = new GUIImage(new RectTransform(new Vector2(0.2f), talentOptionFrame.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight) { MaxSize = new Point(16) }, style: null) + { + CanBeFocused = false, + Color = talentStageBackgroundColors[TalentTree.TalentTreeStageState.Locked] + }; + + Point iconSize = cornerIcon.RectTransform.NonScaledSize; + cornerIcon.RectTransform.AbsoluteOffset = new Point(iconSize.X / 2, iconSize.Y / 2); + + if (subTree.TalentOptionStages.Count <= i) { continue; } - if (subTree.TalentOptionStages.Count > i) - { TalentOption talentOption = subTree.TalentOptionStages[i]; - GUILayoutGroup talentOptionCenterGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 0.7f), talentOptionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft); - GUILayoutGroup talentOptionLayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, talentOptionCenterGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; foreach (TalentPrefab talent in talentOption.Talents.OrderBy(t => t.Identifier)) @@ -1929,6 +2006,7 @@ namespace Barotrauma ToolTip = RichString.Rich(talent.DisplayName + "\n\n" + talent.Description), UserData = talent.Identifier, PressedColor = pressedColor, + Enabled = controlledCharacter != null, OnClicked = (button, userData) => { // deselect other buttons in tier by removing their selected talents from pool @@ -1961,7 +2039,7 @@ namespace Barotrauma }, }; - talentButton.Color = talentButton.HoverColor = talentButton.PressedColor = talentButton.SelectedColor = Color.Transparent; + talentButton.Color = talentButton.HoverColor = talentButton.PressedColor = talentButton.SelectedColor = talentButton.DisabledColor = Color.Transparent; GUIComponent iconImage; if (talent.Icon is null) @@ -1971,6 +2049,7 @@ namespace Barotrauma OutlineColor = GUIStyle.Red, TextColor = GUIStyle.Red, PressedColor = unselectableColor, + DisabledColor = unselectableColor, CanBeFocused = false, }; } @@ -1979,63 +2058,63 @@ namespace Barotrauma iconImage = new GUIImage(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), sprite: talent.Icon, scaleToFit: true) { PressedColor = unselectableColor, + DisabledColor = unselectableColor * 0.5f, CanBeFocused = false, }; } - + iconImage.Enabled = talentButton.Enabled; talentButtons.Add((talentButton, iconImage)); } - - talentCornerIcons.Add((subTree.Identifier, i, cornerIcon, talentBackground, talentBackgroundHighlight)); + talentCornerIcons.Add((subTree.Identifier, i, cornerIcon, talentBackground, talentBackgroundHighlight)); } } - } - GUITextBlock.AutoScaleAndNormalize(subTreeNames); + GUITextBlock.AutoScaleAndNormalize(subTreeNames); - GUILayoutGroup talentBottomFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.07f), talentFrameLayoutGroup.RectTransform, Anchor.TopCenter), isHorizontal: true) { RelativeSpacing = 0.01f }; + GUILayoutGroup talentBottomFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.07f), talentFrameLayoutGroup.RectTransform, Anchor.TopCenter), isHorizontal: true) { RelativeSpacing = 0.01f }; - GUILayoutGroup experienceLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.59f, 1f), talentBottomFrame.RectTransform)); - GUIFrame experienceBarFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), experienceLayout.RectTransform), style: null); + GUILayoutGroup experienceLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.59f, 1f), talentBottomFrame.RectTransform)); + GUIFrame experienceBarFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), experienceLayout.RectTransform), style: null); - experienceBar = new GUIProgressBar(new RectTransform(new Vector2(1f, 1f), experienceBarFrame.RectTransform, Anchor.CenterLeft), - barSize: controlledCharacter.Info.GetProgressTowardsNextLevel(), color: GUIStyle.Green) - { - IsHorizontal = true, - }; + experienceBar = new GUIProgressBar(new RectTransform(new Vector2(1f, 1f), experienceBarFrame.RectTransform, Anchor.CenterLeft), + barSize: info.GetProgressTowardsNextLevel(), color: GUIStyle.Green) + { + IsHorizontal = true, + }; - experienceText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.Font, textAlignment: Alignment.CenterRight) - { - Shadow = true, - ToolTip = TextManager.Get("experiencetooltip") - }; + experienceText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.Font, textAlignment: Alignment.CenterRight) + { + Shadow = true, + ToolTip = TextManager.Get("experiencetooltip") + }; - talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), experienceLayout.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight) { AutoScaleVertical = true }; + talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), experienceLayout.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight) { AutoScaleVertical = true }; - talentResetButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), talentBottomFrame.RectTransform), text: TextManager.Get("reset"), style: "GUIButtonFreeScale") - { - OnClicked = ResetTalentSelection - }; - talentApplyButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), talentBottomFrame.RectTransform), text: TextManager.Get("applysettingsbutton"), style: "GUIButtonFreeScale") - { - OnClicked = ApplyTalentSelection, - }; - GUITextBlock.AutoScaleAndNormalize(talentResetButton.TextBlock, talentApplyButton.TextBlock); + talentResetButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), talentBottomFrame.RectTransform), text: TextManager.Get("reset"), style: "GUIButtonFreeScale") + { + OnClicked = ResetTalentSelection + }; + talentApplyButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), talentBottomFrame.RectTransform), text: TextManager.Get("applysettingsbutton"), style: "GUIButtonFreeScale") + { + OnClicked = ApplyTalentSelection, + }; + GUITextBlock.AutoScaleAndNormalize(talentResetButton.TextBlock, talentApplyButton.TextBlock); + } UpdateTalentInfo(); } - private void CreateTalentSkillList(Character character, GUIListBox parent) + private void CreateTalentSkillList(Character character, CharacterInfo info, GUIListBox parent) { parent.Content.ClearChildren(); List skillNames = new List(); - foreach (Skill skill in character.Info.Job.GetSkills()) + foreach (Skill skill in info.Job.GetSkills()) { GUILayoutGroup skillContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), parent.Content.RectTransform), isHorizontal: true) { CanBeFocused = false }; skillNames.Add(new GUITextBlock(new RectTransform(new Vector2(0.7f, 1f), skillContainer.RectTransform), TextManager.Get($"skillname.{skill.Identifier}").Fallback(skill.Identifier.Value))); new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), Math.Floor(skill.Level).ToString("F0"), textAlignment: Alignment.CenterRight) { Padding = new Vector4(0, 0, 4, 0) }; - float modifiedSkillLevel = character.GetSkillLevel(skill.Identifier); + float modifiedSkillLevel = character?.GetSkillLevel(skill.Identifier) ?? skill.Level; if (!MathUtils.NearlyEqual(MathF.Floor(modifiedSkillLevel), MathF.Floor(skill.Level))) { int skillChange = (int)MathF.Floor(modifiedSkillLevel - skill.Level); @@ -2129,7 +2208,7 @@ namespace Barotrauma talentButton.icon.HoverColor = hoverColor; } - CreateTalentSkillList(controlledCharacter, skillListBox); + CreateTalentSkillList(controlledCharacter, controlledCharacter.Info, skillListBox); } private void ApplyTalents(Character controlledCharacter) @@ -2157,6 +2236,7 @@ namespace Barotrauma private bool ResetTalentSelection(GUIButton guiButton, object userData) { Character controlledCharacter = Character.Controlled; + if (controlledCharacter?.Info == null) { return false; } selectedTalents = controlledCharacter.Info.GetUnlockedTalentsInTree().ToList(); UpdateTalentInfo(); return true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 9d9cb8ac5..db6a05ff0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -546,6 +546,10 @@ namespace Barotrauma yield return CoroutineStatus.Running; +#if DEBUG + LevelGenerationParams.CheckValidity(); +#endif + MainMenuScreen.Select(); foreach (Identifier steamError in SteamManager.InitializationErrors) @@ -1033,11 +1037,6 @@ namespace Barotrauma { GUI.SetSavingIndicatorState(true); - if (GameSession.Submarine != null && !GameSession.Submarine.Removed) - { - GameSession.SubmarineInfo = new SubmarineInfo(GameSession.Submarine); - } - // Update store stock when saving and quitting in an outpost (normally updated when CampaignMode.End() is called) if (GameSession?.Campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedOutpost && spCampaign.Map?.CurrentLocation != null && spCampaign.CargoManager != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 439e43f96..7dbeffd05 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -81,7 +81,6 @@ namespace Barotrauma : this(isSinglePlayer) { AddCharacterElements(element); - ActiveOrdersElement = element.GetChildElement("activeorders"); } partial void InitProjectSpecific() @@ -3661,9 +3660,9 @@ namespace Barotrauma crewList.ClearChildren(); } - public void Save(XElement parentElement) + public XElement Save(XElement parentElement) { - XElement element = new XElement("crew"); + var element = new XElement("crew"); for (int i = 0; i < characterInfos.Count; i++) { var ci = characterInfos[i]; @@ -3674,8 +3673,8 @@ namespace Barotrauma infoElement.Add(new XAttribute("crewlistindex", ci.CrewListIndex)); if (ci.LastControlled) { infoElement.Add(new XAttribute("lastcontrolled", true)); } } - SaveActiveOrders(element); - parentElement.Add(element); + parentElement?.Add(element); + return element; } public static void ClientReadActiveOrders(IReadMessage inc) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs index 8c8832a05..4a91c9026 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs @@ -13,6 +13,11 @@ namespace Barotrauma partial void SettingsChanged(Option balanceChanged, Option rewardChanged) { + if (Owner is Some { Value: var character }) + { + if (!character.IsPlayer) { return; } + } + CampaignMode campaign = GameMain.GameSession?.Campaign; WalletChangedData data = new WalletChangedData { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 53ba428fb..7e7506931 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -92,6 +92,7 @@ namespace Barotrauma break; case "crew": GameMain.GameSession.CrewManager = new CrewManager(subElement, true); + ActiveOrdersElement = element.GetChildElement("activeorders"); break; case "map": map = Map.Load(this, subElement, Settings); @@ -242,11 +243,10 @@ namespace Barotrauma crewDead = false; endTimer = 5.0f; CrewManager.InitSinglePlayerRound(); - if (petsElement != null) - { - PetBehavior.LoadPets(petsElement); - } - CrewManager.LoadActiveOrders(); + LoadPets(); + LoadActiveOrders(); + + CargoManager.InitPurchasedIDCards(); GUI.DisableSavingIndicatorDelayed(); } @@ -461,41 +461,7 @@ namespace Barotrauma if (success) { - if (leavingSub != Submarine.MainSub && !leavingSub.DockedTo.Contains(Submarine.MainSub)) - { - Submarine.MainSub = leavingSub; - GameMain.GameSession.Submarine = leavingSub; - GameMain.GameSession.SubmarineInfo = leavingSub.Info; - leavingSub.Info.FilePath = System.IO.Path.Combine(SaveUtil.TempPath, leavingSub.Info.Name + ".sub"); - var subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub); - GameMain.GameSession.OwnedSubmarines.Add(leavingSub.Info); - foreach (Submarine sub in subsToLeaveBehind) - { - GameMain.GameSession.OwnedSubmarines.RemoveAll(s => s != leavingSub.Info && s.Name == sub.Info.Name); - MapEntity.mapEntityList.RemoveAll(e => e.Submarine == sub && e is LinkedSubmarine); - LinkedSubmarine.CreateDummy(leavingSub, sub); - } - } - - GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - - if (PendingSubmarineSwitch != null) - { - SubmarineInfo previousSub = GameMain.GameSession.SubmarineInfo; - GameMain.GameSession.SubmarineInfo = PendingSubmarineSwitch; - - for (int i = 0; i < GameMain.GameSession.OwnedSubmarines.Count; i++) - { - if (GameMain.GameSession.OwnedSubmarines[i].Name == previousSub.Name) - { - GameMain.GameSession.OwnedSubmarines[i] = previousSub; - break; - } - } - } - SaveUtil.SaveGame(GameMain.GameSession.SavePath); - PendingSubmarineSwitch = null; } else { @@ -766,11 +732,10 @@ namespace Barotrauma c.Info.SaveOrderData(); } - petsElement = new XElement("pets"); - PetBehavior.SavePets(petsElement); - modeElement.Add(petsElement); + SavePets(modeElement); + var crewManagerElement = CrewManager.Save(modeElement); + SaveActiveOrders(crewManagerElement); - CrewManager.Save(modeElement); CampaignMetadata.Save(modeElement); Map.Save(modeElement); CargoManager?.SavePurchasedItems(modeElement); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs index 4f6f7d940..5edeeb26c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs @@ -257,7 +257,7 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(2.0f); }*/ - TriggerTutorialSegment(0, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ToggleInventory)); // Medical supplies objective + TriggerTutorialSegment(0, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect)); // Medical supplies objective do { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs index 80f7ed649..ca52c58ba 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs @@ -275,7 +275,7 @@ namespace Barotrauma.Tutorials do { yield return null; } while (!engineer_equipmentObjectiveSensor.MotionDetected); GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Engineer.Radio.Equipment"), ChatMessageType.Radio, null); yield return new WaitForSeconds(0.5f, false); - TriggerTutorialSegment(0, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ToggleInventory)); // Retrieve equipment + TriggerTutorialSegment(0, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect)); // Retrieve equipment bool firstSlotRemoved = false; bool secondSlotRemoved = false; bool thirdSlotRemoved = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs index a9ea9047a..de6a066f2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs @@ -330,7 +330,7 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(0.0f, false); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.Equipment"), ChatMessageType.Radio, null); do { yield return null; } while (!mechanic_equipmentObjectiveSensor.MotionDetected); - TriggerTutorialSegment(1, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ToggleInventory)); // Equipment & inventory objective + TriggerTutorialSegment(1, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect)); // Equipment & inventory objective SetHighlight(mechanic_equipmentCabinet.Item, true); bool firstSlotRemoved = false; bool secondSlotRemoved = false; @@ -372,7 +372,7 @@ namespace Barotrauma.Tutorials // Room 3 do { yield return null; } while (!mechanic_weldingObjectiveSensor.MotionDetected); - TriggerTutorialSegment(2, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Aim), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ToggleInventory)); // Welding objective + TriggerTutorialSegment(2, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Aim), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot)); // Welding objective do { if (!mechanic.HasEquippedItem("divingmask".ToIdentifier())) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs index 60369b132..5c9061d8e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs @@ -106,7 +106,7 @@ namespace Barotrauma.Tutorials Character.Controlled = character; character.GiveJobItems(null); - var idCard = character.Inventory.FindItemByIdentifier("idcard".ToIdentifier()); + var idCard = character.Inventory.FindItemByTag("identitycard".ToIdentifier()); if (idCard == null) { DebugConsole.ThrowError("Item prefab \"ID Card\" not found!"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 4746f0e9f..2245ad99d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -525,6 +525,7 @@ namespace Barotrauma if (!AccessibleWhenAlive && !character.IsDead && !AccessibleByOwner) { syncItemsDelay = Math.Max(syncItemsDelay - deltaTime, 0.0f); + doubleClickedItems.Clear(); return; } @@ -931,7 +932,7 @@ namespace Barotrauma // Move the item from the subinventory to the selected container return QuickUseAction.PutToContainer; } - else + else if (character.Inventory.AccessibleWhenAlive || character.Inventory.AccessibleByOwner) { // Take from the subinventory and place it in the character's main inventory if no target container is selected return QuickUseAction.TakeFromContainer; @@ -959,7 +960,8 @@ namespace Barotrauma } else if (character.SelectedBy?.Inventory != null && Character.Controlled == character.SelectedBy && - !character.SelectedBy.Inventory.Locked && + !character.SelectedBy.Inventory.Locked && + (character.SelectedBy.Inventory.AccessibleWhenAlive || character.SelectedBy.Inventory.AccessibleByOwner) && allowInventorySwap) { return QuickUseAction.TakeFromCharacter; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index 2b372fd27..9ea53f8da 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -66,6 +66,9 @@ namespace Barotrauma.Items.Components rect.Height = (int)(rect.Height * (1.0f - openState)); } + //only merge the door's convex hull with overlapping wall segments if it's fully open or fully closed + //it's the heaviest part of changing the convex hull, and doesn't need to be done while the door is still in motion + bool mergeOverlappingSegments = openState <= 0.0f || openState >= 1.0f; if (Window.Height > 0 && Window.Width > 0) { if (IsHorizontal) @@ -88,7 +91,7 @@ namespace Barotrauma.Items.Components else { convexHull2.Enabled = true; - convexHull2.SetVertices(GetConvexHullCorners(rect2)); + convexHull2.SetVertices(GetConvexHullCorners(rect2), mergeOverlappingSegments); } } } @@ -112,7 +115,7 @@ namespace Barotrauma.Items.Components else { convexHull2.Enabled = true; - convexHull2.SetVertices(GetConvexHullCorners(rect2)); + convexHull2.SetVertices(GetConvexHullCorners(rect2), mergeOverlappingSegments); } } } @@ -127,7 +130,7 @@ namespace Barotrauma.Items.Components else { convexHull.Enabled = true; - convexHull.SetVertices(GetConvexHullCorners(rect)); + convexHull.SetVertices(GetConvexHullCorners(rect), mergeOverlappingSegments); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index 85791f95b..2a6d4328e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System.Linq; @@ -78,7 +79,7 @@ namespace Barotrauma.Items.Components activateButton = new GUIButton(new RectTransform(new Vector2(0.95f, 0.8f), buttonContainer.RectTransform), TextManager.Get("DeconstructorDeconstruct"), style: "DeviceButton") { TextBlock = { AutoScaleHorizontal = true }, - OnClicked = ToggleActive + OnClicked = OnActivateButtonClicked }; inSufficientPowerWarning = new GUITextBlock(new RectTransform(Vector2.One, activateButton.RectTransform), TextManager.Get("DeconstructorNoPower"), textColor: GUIStyle.Orange, textAlignment: Alignment.Center, color: Color.Black, style: "OuterGlow", wrap: true) @@ -164,7 +165,7 @@ namespace Barotrauma.Items.Components } } } - activateButton.Enabled = outputsFound; + activateButton.Enabled = outputsFound || !InputContainer.Inventory.IsEmpty(); activateButton.Text = TextManager.Get(ActivateButtonText); }; } @@ -236,8 +237,19 @@ namespace Barotrauma.Items.Components inSufficientPowerWarning.Visible = IsActive && !hasPower; } - private bool ToggleActive(GUIButton button, object obj) + private bool OnActivateButtonClicked(GUIButton button, object obj) { + var disallowedItem = inputContainer.Inventory.FindItem(i => !i.AllowDeconstruct, recursive: false); + if (disallowedItem != null) + { + int index = inputContainer.Inventory.FindIndex(disallowedItem); + if (index >= 0 && index < inputContainer.Inventory.visualSlots.Length) + { + var slot = inputContainer.Inventory.visualSlots[index]; + slot?.ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f); + } + return true; + } if (GameMain.Client != null) { pendingState = !IsActive; @@ -247,7 +259,6 @@ namespace Barotrauma.Items.Components { SetActive(!IsActive, Character.Controlled); } - return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 90f0e04b3..0b3583f3b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -1367,6 +1367,15 @@ namespace Barotrauma.Items.Components pingRadius, prevPingRadius, 250.0f, 150.0f, range, pingStrength, passive); } + if (pingSource.Y - Level.Loaded.BottomPos < range) + { + CreateBlipsForLine( + new Vector2(pingSource.X - range, Level.Loaded.BottomPos), + new Vector2(pingSource.X + range, Level.Loaded.BottomPos), + pingSource, transducerPos, + pingRadius, prevPingRadius, + 250.0f, 150.0f, range, pingStrength, passive); + } List cells = Level.Loaded.GetCells(pingSource, 7); foreach (Voronoi2.VoronoiCell cell in cells) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs index 759f67ad6..a449dc00f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs @@ -23,10 +23,10 @@ namespace Barotrauma.Items.Components } #endif - private List particleEmitters = new List(); - private List particleEmitterHitStructure = new List(); - private List particleEmitterHitCharacter = new List(); - private List> particleEmitterHitItem = new List>(); + private readonly List particleEmitters = new List(); + private readonly List particleEmitterHitStructure = new List(); + private readonly List particleEmitterHitCharacter = new List(); + private readonly List<(RelatedItem relatedItem, ParticleEmitter emitter)> particleEmitterHitItem = new List<(RelatedItem relatedItem, ParticleEmitter emitter)>(); private float prevProgressBarState; private Item prevProgressBarTarget = null; @@ -46,10 +46,7 @@ namespace Barotrauma.Items.Components Identifier[] excludedIdentifiers = subElement.GetAttributeIdentifierArray("excludedidentifiers", Array.Empty()); if (excludedIdentifiers.Length == 0) { excludedIdentifiers = subElement.GetAttributeIdentifierArray("excludedidentifier", Array.Empty()); } - particleEmitterHitItem.Add( - new Pair( - new RelatedItem(identifiers, excludedIdentifiers), - new ParticleEmitter(subElement))); + particleEmitterHitItem.Add((new RelatedItem(identifiers, excludedIdentifiers), new ParticleEmitter(subElement))); break; case "particleemitterhitstructure": particleEmitterHitStructure.Add(new ParticleEmitter(subElement)); @@ -139,11 +136,11 @@ namespace Barotrauma.Items.Components Vector2 particlePos = ConvertUnits.ToDisplayUnits(pickedPosition); if (targetItem.Submarine != null) particlePos += targetItem.Submarine.DrawPosition; - foreach (var emitter in particleEmitterHitItem) + foreach ((RelatedItem relatedItem, ParticleEmitter emitter) in particleEmitterHitItem) { - if (!emitter.First.MatchesItem(targetItem)) { continue; } + if (!relatedItem.MatchesItem(targetItem)) { continue; } float particleAngle = item.body.Rotation + MathHelper.ToRadians(BarrelRotation) + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); - emitter.Second.Emit(deltaTime, particlePos, item.CurrentHull, particleAngle + MathHelper.Pi, -particleAngle + MathHelper.Pi); + emitter.Emit(deltaTime, particlePos, item.CurrentHull, particleAngle + MathHelper.Pi, -particleAngle + MathHelper.Pi); } } #if DEBUG diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index 93c653f0b..8d5d16766 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -38,7 +38,7 @@ namespace Barotrauma.Items.Components int totalWireCount = 0; foreach (Connection c in panel.Connections) { - totalWireCount += c.Wires.Count(w => w != null); + totalWireCount += c.Wires.Count; } Wire equippedWire = null; @@ -87,8 +87,8 @@ namespace Barotrauma.Items.Components (DraggingConnected.Connections[0] == null && DraggingConnected.Connections[1] == null) || (DraggingConnected.Connections.Contains(c) && DraggingConnected.Connections.Contains(null))) { - int linkIndex = c.FindWireIndex(DraggingConnected.Item); - if (linkIndex > -1 || panel.DisconnectedWires.Contains(DraggingConnected)) + var linkedWire = c.FindWireByItem(DraggingConnected.Item); + if (linkedWire != null || panel.DisconnectedWires.Contains(DraggingConnected)) { Inventory.DraggingItems.Clear(); Inventory.DraggingItems.Add(DraggingConnected.Item); @@ -108,7 +108,7 @@ namespace Barotrauma.Items.Components c.DrawWires(spriteBatch, panel, rightPos, rightWirePos, mouseInRect, equippedWire, wireInterval); } rightPos.Y += connectorIntervalLeft; - rightWirePos.Y += c.Wires.Count(w => w != null) * wireInterval; + rightWirePos.Y += c.Wires.Count * wireInterval; } else { @@ -121,7 +121,7 @@ namespace Barotrauma.Items.Components c.DrawWires(spriteBatch, panel, leftPos, leftWirePos, mouseInRect, equippedWire, wireInterval); } leftPos.Y += connectorIntervalRight; - leftWirePos.Y += c.Wires.Count(w => w != null) * wireInterval; + leftWirePos.Y += c.Wires.Count * wireInterval; } } } @@ -228,15 +228,15 @@ namespace Barotrauma.Items.Components { float connectorSpriteScale = (35.0f / connectionSprite.SourceRect.Width) * panel.Scale; - for (int i = 0; i < MaxWires; i++) + foreach (var wire in wires) { - if (wires[i] == null || wires[i].Hidden || (DraggingConnected == wires[i] && (mouseIn || Screen.Selected == GameMain.SubEditorScreen))) { continue; } - if (wires[i].HiddenInGame && Screen.Selected == GameMain.GameScreen) { continue; } + if (wire.Hidden || (DraggingConnected == wire && (mouseIn || Screen.Selected == GameMain.SubEditorScreen))) { continue; } + if (wire.HiddenInGame && Screen.Selected == GameMain.GameScreen) { continue; } - Connection recipient = wires[i].OtherConnection(this); + Connection recipient = wire.OtherConnection(this); LocalizedString label = recipient == null ? "" : recipient.item.Name + $" ({recipient.DisplayName})"; - if (wires[i].Locked) { label += "\n" + TextManager.Get("ConnectionLocked"); } - DrawWire(spriteBatch, wires[i], position, wirePosition, equippedWire, panel, label); + if (wire.Locked) { label += "\n" + TextManager.Get("ConnectionLocked"); } + DrawWire(spriteBatch, wire, position, wirePosition, equippedWire, panel, label); wirePosition.Y += wireInterval; } @@ -248,18 +248,17 @@ namespace Barotrauma.Items.Components if (!PlayerInput.PrimaryMouseButtonHeld()) { if ((GameMain.NetworkMember != null || panel.CheckCharacterSuccess(Character.Controlled)) && - Wires.Count(w => w != null) < MaxPlayerConnectableWires) + Wires.Count < MaxPlayerConnectableWires) { //find an empty cell for the new connection - int index = FindEmptyIndex(); - if (index > -1 && !Wires.Contains(DraggingConnected)) + if (WireSlotsAvailable() && !Wires.Contains(DraggingConnected)) { bool alreadyConnected = DraggingConnected.IsConnectedTo(panel.Item); DraggingConnected.RemoveConnection(panel.Item); if (DraggingConnected.Connect(this, !alreadyConnected, true)) { var otherConnection = DraggingConnected.OtherConnection(this); - SetWire(index, DraggingConnected); + ConnectWire(DraggingConnected); } } } @@ -284,7 +283,7 @@ namespace Barotrauma.Items.Components flashColor * (float)Math.Sin(FlashTimer % flashCycleDuration / flashCycleDuration * MathHelper.Pi * 0.8f), scale: connectorSpriteScale); } - if (Wires.Any(w => w != null && w != DraggingConnected && !w.Hidden && (!w.HiddenInGame || Screen.Selected != GameMain.GameScreen))) + if (Wires.Any(w => w != DraggingConnected && !w.Hidden && (!w.HiddenInGame || Screen.Selected != GameMain.GameScreen))) { int screwIndex = (int)Math.Floor(position.Y / 30.0f) % screwSprites.Count; screwSprites[screwIndex].Draw(spriteBatch, position, scale: connectorSpriteScale); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs index 0d026f53b..21b3b4204 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs @@ -77,7 +77,7 @@ namespace Barotrauma.Items.Components } } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { if (item.Submarine == null || item.Submarine.Loading || Screen.Selected != GameMain.SubEditorScreen) { return; } MoveConnectedWires(amount); @@ -173,9 +173,8 @@ namespace Barotrauma.Items.Components private void ApplyRemoteState(IReadMessage msg) { - List prevWires = Connections.SelectMany(c => c.Wires.Where(w => w != null)).ToList(); - List newWires = new List(); - + List prevWires = Connections.SelectMany(c => c.Wires).ToList(); + ushort userID = msg.ReadUInt16(); if (userID == 0) @@ -195,7 +194,9 @@ namespace Barotrauma.Items.Components foreach (Connection connection in Connections) { - for (int i = 0; i < connection.MaxWires; i++) + HashSet newWires = new HashSet(); + uint wireCount = msg.ReadVariableUInt32(); + for (int i = 0; i < wireCount; i++) { ushort wireId = msg.ReadUInt16(); @@ -204,9 +205,18 @@ namespace Barotrauma.Items.Components if (wireComponent == null) { continue; } newWires.Add(wireComponent); + } - connection.SetWire(i, wireComponent); - wireComponent.Connect(connection, false); + Wire[] oldWires = connection.Wires.Where(w => !newWires.Contains(w)).ToArray(); + foreach (var wire in oldWires) + { + connection.DisconnectWire(wire); + } + + foreach (var wire in newWires.Where(w => !connection.Wires.Contains(w)).ToArray()) + { + connection.ConnectWire(wire); + wire.Connect(connection, false); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index 50a7f11d3..1853d569c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -205,7 +205,7 @@ namespace Barotrauma.Items.Components foreach (var uiElement in uiElements) { if (!(uiElement.UserData is CustomInterfaceElement element)) { continue; } - bool visible = Screen.Selected == GameMain.SubEditorScreen || element.StatusEffects.Any() || element.HasPropertyName || (element.Connection != null && element.Connection.Wires.Any(w => w != null)); + bool visible = Screen.Selected == GameMain.SubEditorScreen || element.StatusEffects.Any() || element.HasPropertyName || (element.Connection != null && element.Connection.Wires.Count > 0); if (visible) { visibleElementCount++; } if (uiElement.Visible != visible) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index fcede0408..e0b3a75f0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -526,7 +526,7 @@ namespace Barotrauma.Items.Components } } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { //only used in the sub editor, hence only in the client project if (!item.IsSelected) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index 42fc88545..919eae413 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -175,7 +175,7 @@ namespace Barotrauma.Items.Components }; } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { widgets.Clear(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs index c8eb6b435..9466e4377 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs @@ -6,8 +6,10 @@ using Microsoft.Xna.Framework.Graphics; namespace Barotrauma.Items.Components { - partial class DockingPort : ItemComponent, IDrawableComponent, IServerSerializable + partial class DockingPort : ItemComponent, IDrawableComponent, IServerSerializable, IClientSerializable { + private GUIMessageBox autodockingVerification; + public Vector2 DrawSize { //use the extents of the item as the draw size @@ -180,5 +182,10 @@ namespace Barotrauma.Items.Components Undock(); } } + + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) + { + msg.Write((byte)allowOutpostAutoDocking); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 96c563ea2..b23835fec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -265,7 +265,7 @@ namespace Barotrauma else { LocalizedString description = item.Description; - if (item.Prefab.Identifier == "idcard" || item.Tags.Contains("despawncontainer")) + if (item.HasTag("identitycard") || item.HasTag("despawncontainer")) { string[] readTags = item.Tags.Split(','); string idName = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 7a16fca75..e7cc950ab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -1,4 +1,6 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Barotrauma.MapCreatures.Behavior; using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; @@ -6,13 +8,8 @@ using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using Barotrauma.Extensions; -using Barotrauma.MapCreatures.Behavior; -using FarseerPhysics.Dynamics; -using FarseerPhysics.Dynamics.Contacts; using System.Collections.Immutable; +using System.Linq; namespace Barotrauma { @@ -720,7 +717,7 @@ namespace Barotrauma //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.Any(ip => ip.Identifier == t)) + .Where(t => !ItemPrefab.Prefabs.ContainsKey(t)) .ToImmutableHashSet(); new GUIButton(new RectTransform(new Vector2(0.1f, 1), tagsField.RectTransform, Anchor.TopRight), "...") { @@ -1174,7 +1171,7 @@ namespace Barotrauma texts.Clear(); string nameText = Name; - if (Prefab.Identifier == "idcard" || Tags.Contains("despawncontainer")) + if (Prefab.Tags.Contains("identitycard") || Tags.Contains("despawncontainer")) { string[] readTags = Tags.Split(','); string idName = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs index ed2f608d8..797801633 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs @@ -80,15 +80,17 @@ namespace Barotrauma } Vector2 center = new Vector2((minX + maxX) / 2.0f, (minY + maxY) / 2.0f); if (Submarine.MainSub != null) { center -= Submarine.MainSub.HiddenSubPosition; } - center.X -= MathUtils.RoundTowardsClosest(center.X, Submarine.GridSize.X); - center.Y -= MathUtils.RoundTowardsClosest(center.Y, Submarine.GridSize.Y); + + Vector2 offsetFromGrid = new Vector2( + MathUtils.RoundTowardsClosest(center.X, Submarine.GridSize.X) - center.X, + MathUtils.RoundTowardsClosest(center.Y, Submarine.GridSize.Y) - center.Y - Submarine.GridSize.Y / 2); MapEntity.SelectedList.Clear(); assemblyEntities.ForEach(e => MapEntity.AddSelection(e)); foreach (MapEntity mapEntity in assemblyEntities) { - mapEntity.Move(-center); + mapEntity.Move(-center - offsetFromGrid); mapEntity.Submarine = Submarine.MainSub; var entityElement = mapEntity.Save(element); if (disabledEntities.Contains(mapEntity)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index 0483137ee..27b1aaf28 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -461,7 +461,7 @@ namespace Barotrauma.Lights Matrix.CreateTranslation(-origin.X, -origin.Y, 0.0f) * Matrix.CreateRotationZ(amount) * Matrix.CreateTranslation(origin.X, origin.Y, 0.0f); - SetVertices(vertices.Select(v => v.Pos).ToArray(), rotationMatrix); + SetVertices(vertices.Select(v => v.Pos).ToArray(), rotationMatrix: rotationMatrix); } private void CalculateDimensions() @@ -541,7 +541,7 @@ namespace Barotrauma.Lights } } - public void SetVertices(Vector2[] points, Matrix? rotationMatrix = null) + public void SetVertices(Vector2[] points, bool mergeOverlappingSegments = true, Matrix? rotationMatrix = null) { Debug.Assert(points.Length == 4, "Only rectangular convex hulls are supported"); @@ -594,13 +594,16 @@ namespace Barotrauma.Lights if (ParentEntity == null) { return; } - var chList = HullLists.Find(h => h.Submarine == ParentEntity.Submarine); - if (chList != null) + if (mergeOverlappingSegments) { - overlappingHulls.Clear(); - foreach (ConvexHull ch in chList.List) + var chList = HullLists.Find(h => h.Submarine == ParentEntity.Submarine); + if (chList != null) { - MergeOverlappingSegments(ch); + overlappingHulls.Clear(); + foreach (ConvexHull ch in chList.List) + { + MergeOverlappingSegments(ch); + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs index 5f4645cac..365db83ac 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs @@ -81,11 +81,18 @@ namespace Barotrauma } catch (System.IO.FileNotFoundException e) { - string errorMsg = "Failed to load sound file \"" + filename + "\"."; + string errorMsg = "Failed to load sound file \"" + filename + "\" (file not found)."; DebugConsole.ThrowError(errorMsg, e); GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:FileNotFound" + filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); return null; } + catch (System.IO.InvalidDataException e) + { + string errorMsg = "Failed to load sound file \"" + filename + "\" (invalid data)."; + DebugConsole.ThrowError(errorMsg, e); + GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:InvalidData" + filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); + return null; + } } RoundSound newSound = new RoundSound(element, existingSound); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index eff30e678..45218c779 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -525,7 +525,7 @@ namespace Barotrauma Item.ItemList.Count(it2 => it2.linkedTo.Contains(item) && !item.linkedTo.Contains(it2)); for (int i = 0; i < item.Connections.Count; i++) { - int wireCount = item.Connections[i].Wires.Count(w => w != null); + int wireCount = item.Connections[i].Wires.Count; if (doorLinks + wireCount > item.Connections[i].MaxWires) { errorMsgs.Add(TextManager.GetWithVariables("InsufficientFreeConnectionsWarning", diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index 9e0472592..5e22aaa33 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; using System; namespace Barotrauma.Networking @@ -183,6 +184,11 @@ namespace Barotrauma.Networking break; default: GameMain.Client.AddChatMessage(txt, type, senderName, senderClient, senderCharacter, changeType, textColor: textColor); + if (type == ChatMessageType.Radio && CanUseRadio(senderCharacter, out WifiComponent radio)) + { + Signal s = new Signal(txt, sender: senderCharacter, source: radio.Item); + radio.TransmitSignal(s, sentFromChat: true); + } break; } LastID = id; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index ce27b16ae..858971e33 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -3,6 +3,7 @@ using Barotrauma.Steam; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using Barotrauma.IO; using System.IO.Compression; using System.Linq; @@ -11,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; using Barotrauma.Extensions; +using Microsoft.Xna.Framework.Input; namespace Barotrauma.Networking { @@ -182,6 +184,20 @@ namespace Barotrauma.Networking get { return ownerKey > 0 || steamP2POwner; } } + internal readonly struct PermissionChangedEvent + { + public readonly ClientPermissions NewPermissions; + public readonly ImmutableArray NewPermittedConsoleCommands; + + public PermissionChangedEvent(ClientPermissions newPermissions, IReadOnlyList newPermittedConsoleCommands) + { + NewPermissions = newPermissions; + NewPermittedConsoleCommands = newPermittedConsoleCommands.ToImmutableArray(); + } + } + + public readonly NamedEvent OnPermissionChanged = new NamedEvent(); + public GameClient(string newName, string ip, UInt64 steamId, string serverName = null, int ownerKey = 0, bool steamP2POwner = false) { //TODO: gui stuff should probably not be here? @@ -570,7 +586,12 @@ namespace Barotrauma.Networking public override void Update(float deltaTime) { #if DEBUG - if (PlayerInput.GetKeyboardState.IsKeyDown(Microsoft.Xna.Framework.Input.Keys.P)) return; + if (PlayerInput.GetKeyboardState.IsKeyDown(Keys.P)) return; + + if (PlayerInput.KeyHit(Keys.Home)) + { + OnPermissionChanged.Invoke(new PermissionChangedEvent(permissions, permittedConsoleCommands)); + } #endif foreach (Client c in ConnectedClients) @@ -1019,40 +1040,25 @@ namespace Barotrauma.Networking GameMain.GameSession.EnforceMissionOrder(serverMissionIdentifiers); } - byte equalityCheckValueCount = inc.ReadByte(); - List levelEqualityCheckValues = new List(); - for (int i = 0; i < equalityCheckValueCount; i++) + var levelEqualityCheckValues = new Dictionary(); + foreach (Level.LevelGenStage stage in Enum.GetValues(typeof(Level.LevelGenStage)).OfType().OrderBy(s => s)) { - levelEqualityCheckValues.Add(inc.ReadInt32()); + levelEqualityCheckValues.Add(stage, inc.ReadInt32()); } - if (Level.Loaded.EqualityCheckValues.Count != levelEqualityCheckValues.Count) + foreach (var stage in levelEqualityCheckValues.Keys) { - string errorMsg = "Level equality check failed. The level generated at your end doesn't match the level generated by the server" + - " (client value count: " + Level.Loaded.EqualityCheckValues.Count + - ", level value count: " + levelEqualityCheckValues.Count + - ", seed: " + Level.Loaded.Seed + - ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortRepresentation + ")" + - ", mirrored: " + Level.Loaded.Mirrored + ")."; - GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - throw new Exception(errorMsg); - } - else - { - for (int i = 0; i < equalityCheckValueCount; i++) + if (Level.Loaded.EqualityCheckValues[stage] != levelEqualityCheckValues[stage]) { - if (Level.Loaded.EqualityCheckValues[i] != levelEqualityCheckValues[i]) - { - string errorMsg = "Level equality check failed. The level generated at your end doesn't match the level generated by the server" + - " (client value #" + i + ": " + Level.Loaded.EqualityCheckValues[i] + - ", server value #" + i + ": " + levelEqualityCheckValues[i].ToString("X") + - ", level value count: " + levelEqualityCheckValues.Count + - ", seed: " + Level.Loaded.Seed + - ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortRepresentation + ")" + - ", mirrored: " + Level.Loaded.Mirrored + ")."; - GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - throw new Exception(errorMsg); - } + string errorMsg = "Level equality check failed. The level generated at your end doesn't match the level generated by the server" + + " (client value " + stage + ": " + Level.Loaded.EqualityCheckValues[stage].ToString("X") + + ", server value " + stage + ": " + levelEqualityCheckValues[stage].ToString("X") + + ", level value count: " + levelEqualityCheckValues.Count + + ", seed: " + Level.Loaded.Seed + + ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortRepresentation + ")" + + ", mirrored: " + Level.Loaded.Mirrored + ")."; + GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + throw new Exception(errorMsg); } } @@ -1076,6 +1082,7 @@ namespace Barotrauma.Networking reconnectBox?.Close(); reconnectBox = null; + GameMain.ModDownloadScreen.Reset(); ContentPackageManager.EnabledPackages.Restore(); GUI.ClearCursorWait(); @@ -1380,18 +1387,13 @@ namespace Barotrauma.Networking private void SetMyPermissions(ClientPermissions newPermissions, IEnumerable permittedConsoleCommands) { if (!(this.permittedConsoleCommands.Any(c => !permittedConsoleCommands.Contains(c)) || - permittedConsoleCommands.Any(c => !this.permittedConsoleCommands.Contains(c)))) + permittedConsoleCommands.Any(c => !this.permittedConsoleCommands.Contains(c)))) { if (newPermissions == permissions) return; } - bool refreshCampaignUI = false; - - if (permissions.HasFlag(ClientPermissions.ManageCampaign) != newPermissions.HasFlag(ClientPermissions.ManageCampaign) || - permissions.HasFlag(ClientPermissions.ManageRound) != newPermissions.HasFlag(ClientPermissions.ManageRound)) - { - refreshCampaignUI = true; - } + bool refreshCampaignUI = permissions.HasFlag(ClientPermissions.ManageCampaign) != newPermissions.HasFlag(ClientPermissions.ManageCampaign) || + permissions.HasFlag(ClientPermissions.ManageRound) != newPermissions.HasFlag(ClientPermissions.ManageRound); permissions = newPermissions; this.permittedConsoleCommands = new List(permittedConsoleCommands); @@ -1430,7 +1432,7 @@ namespace Barotrauma.Networking if (newPermissions.HasFlag(ClientPermissions.ConsoleCommands)) { var commandsLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), rightColumn.RectTransform), - TextManager.Get("PermittedConsoleCommands"), wrap: true, font: GUIStyle.SubHeadingFont); + 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) { @@ -1469,6 +1471,7 @@ namespace Barotrauma.Networking } GameMain.NetLobbyScreen.RefreshEnabledElements(); + OnPermissionChanged.Invoke(new PermissionChangedEvent(permissions, this.permittedConsoleCommands)); } private IEnumerable StartGame(IReadMessage inc) @@ -3680,7 +3683,9 @@ namespace Barotrauma.Networking } if (Level.Loaded != null) { - errorLines.Add("Level: " + Level.Loaded.Seed + ", " + string.Join(", ", Level.Loaded.EqualityCheckValues.Select(cv => cv.ToString("X")))); + errorLines.Add("Level: " + Level.Loaded.Seed + ", " + + string.Join("; ", Level.Loaded.EqualityCheckValues.Select(cv + => cv.Key + "=" + cv.Value.ToString("X")))); errorLines.Add("Entity count before generating level: " + Level.Loaded.EntityCountBeforeGenerate); errorLines.Add("Entities:"); foreach (Entity e in Level.Loaded.EntitiesBeforeGenerate.OrderBy(e => e.CreationIndex)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index 430df186b..13f093216 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -621,7 +621,7 @@ namespace Barotrauma.Networking { Stretch = true }; - + var losModeRadioButtonGroup = new GUIRadioButtonGroup(); LosMode[] losModes = (LosMode[])Enum.GetValues(typeof(LosMode)); for (int i = 0; i < losModes.Length; i++) @@ -634,6 +634,14 @@ namespace Barotrauma.Networking var traitorsMinPlayerCount = CreateLabeledNumberInput(roundsTab, "ServerSettingsTraitorsMinPlayerCount", 1, 16, "ServerSettingsTraitorsMinPlayerCountToolTip"); GetPropertyData(nameof(TraitorsMinPlayerCount)).AssignGUIComponent(traitorsMinPlayerCount); + var maximumTransferAmount = CreateLabeledNumberInput(roundsTab, "serversettingsmaximumtransferrequest", 0, CampaignMode.MaxMoney, "serversettingsmaximumtransferrequesttooltip"); + GetPropertyData(nameof(MaximumTransferRequest)).AssignGUIComponent(maximumTransferAmount); + + var lootedMoneyDestination = CreateLabeledDropdown(roundsTab, "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 ragdollButtonBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsAllowRagdollButton")); GetPropertyData(nameof(AllowRagdollButton)).AssignGUIComponent(ragdollButtonBox); @@ -991,6 +999,32 @@ namespace Barotrauma.Networking 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; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs index fa77bcd3e..8af7d4e14 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs @@ -103,7 +103,7 @@ namespace Barotrauma.Particles { return debugName; } - public void Init(ParticlePrefab prefab, Vector2 position, Vector2 speed, float rotation, Hull hullGuess = null, bool drawOnTop = false, float collisionIgnoreTimer = 0f, Tuple tracerPoints = null) + public void Init(ParticlePrefab prefab, Vector2 position, Vector2 speed, float rotation, Hull hullGuess = null, bool drawOnTop = false, float collisionIgnoreTimer = 0f, float lifeTimeMultiplier = 1f, Tuple tracerPoints = null) { this.prefab = prefab; #if DEBUG @@ -149,13 +149,13 @@ namespace Barotrauma.Particles if (prefab.LifeTimeMin <= 0.0f) { - totalLifeTime = prefab.LifeTime; - lifeTime = prefab.LifeTime; + totalLifeTime = prefab.LifeTime * lifeTimeMultiplier; + lifeTime = prefab.LifeTime * lifeTimeMultiplier; } else { - totalLifeTime = Rand.Range(prefab.LifeTimeMin, prefab.LifeTime); - lifeTime = totalLifeTime; + totalLifeTime = Rand.Range(prefab.LifeTimeMin, prefab.LifeTime) * lifeTimeMultiplier; + lifeTime = totalLifeTime * lifeTimeMultiplier; } startDelay = Rand.Range(prefab.StartDelayMin, prefab.StartDelayMax); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index f0457e030..cae3906e3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -85,6 +85,9 @@ namespace Barotrauma.Particles [Editable, Serialize("1,1,1,1", IsPropertySaveable.Yes)] public Color ColorMultiplier { get; set; } + [Editable, Serialize(1f, IsPropertySaveable.Yes)] + public float LifeTimeMultiplier { get; set; } + [Editable, Serialize(false, IsPropertySaveable.Yes)] public bool DrawOnTop { get; set; } @@ -197,7 +200,7 @@ namespace Barotrauma.Particles position += dir * Rand.Range(Prefab.Properties.DistanceMin, Prefab.Properties.DistanceMax); } - var particle = GameMain.ParticleManager.CreateParticle(particlePrefab, position, velocity, particleRotation, hullGuess, Prefab.DrawOnTop, tracerPoints: tracerPoints); + var particle = GameMain.ParticleManager.CreateParticle(particlePrefab, position, velocity, particleRotation, hullGuess, Prefab.DrawOnTop, lifeTimeMultiplier: Prefab.Properties.LifeTimeMultiplier, tracerPoints: tracerPoints); if (particle != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs index 558faee69..982106567 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs @@ -76,7 +76,7 @@ namespace Barotrauma.Particles return CreateParticle(prefab, position, velocity, rotation, hullGuess, collisionIgnoreTimer: collisionIgnoreTimer, tracerPoints:tracerPoints); } - public Particle CreateParticle(ParticlePrefab prefab, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, bool drawOnTop = false, float collisionIgnoreTimer = 0f, Tuple tracerPoints = null) + public Particle CreateParticle(ParticlePrefab prefab, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, bool drawOnTop = false, float collisionIgnoreTimer = 0f, float lifeTimeMultiplier = 1f, Tuple tracerPoints = null) { if (prefab == null || prefab.Sprites.Count == 0) { return null; } @@ -115,7 +115,7 @@ namespace Barotrauma.Particles if (particles[particleCount] == null) { particles[particleCount] = new Particle(); } - particles[particleCount].Init(prefab, position, velocity, rotation, hullGuess, drawOnTop, collisionIgnoreTimer, tracerPoints: tracerPoints); + particles[particleCount].Init(prefab, position, velocity, rotation, hullGuess, drawOnTop, collisionIgnoreTimer, lifeTimeMultiplier, tracerPoints: tracerPoints); particleCount++; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 9df67c87e..f8c3d29c9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -77,9 +77,20 @@ namespace Barotrauma if (remoteContentDoc?.Root != null) { remoteContentContainer.ClearChildren(); - foreach (var subElement in remoteContentDoc.Root.Elements()) + try { - GUIComponent.FromXML(subElement.FromPackage(null), remoteContentContainer.RectTransform); + foreach (var subElement in remoteContentDoc.Root.Elements()) + { + GUIComponent.FromXML(subElement.FromPackage(null), remoteContentContainer.RectTransform); + } + } + catch (Exception e) + { +#if DEBUG + DebugConsole.ThrowError("Reading received remote main menu content failed.", e); +#endif + GameAnalyticsManager.AddErrorEventOnce("MainMenuScreen.RemoteContentParse:Exception", GameAnalyticsManager.ErrorSeverity.Error, + "Reading received remote main menu content failed. " + e.Message); } } }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs index 0656aacea..537124f37 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -20,10 +20,11 @@ namespace Barotrauma private ServerContentPackage? currentDownload; private readonly List downloadedPackages = new List(); + public IEnumerable DownloadedPackages => downloadedPackages; private bool confirmDownload; - private void Reset() + public void Reset() { pendingDownloads.Clear(); downloadedPackages.Clear(); @@ -255,12 +256,6 @@ namespace Barotrauma } } - public override void Deselect() - { - Reset(); - base.Deselect(); - } - public override void Update(double deltaTime) { base.Update(deltaTime); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 2e9797dba..ff6f3240e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -1400,7 +1400,7 @@ namespace Barotrauma public void CreatePlayerFrame(GUIComponent parent, bool createPendingText = true, bool alwaysAllowEditing = false) { UpdatePlayerFrame( - Character.Controlled?.Info ?? playerInfoContainer.Children?.First().UserData as CharacterInfo, + Character.Controlled?.Info ?? playerInfoContainer.Children?.First().UserData as CharacterInfo ?? GameMain.Client.CharacterInfo, allowEditing: alwaysAllowEditing || campaignCharacterInfo == null, parent: parent, createPendingText: createPendingText); @@ -3131,10 +3131,11 @@ namespace Barotrauma retVal[i] = new GUIImage[outfitPreview.Sprites.Count]; for (int j = 0; j < outfitPreview.Sprites.Count; j++) { - Pair sprite = outfitPreview.Sprites[j]; + Sprite sprite = outfitPreview.Sprites[j].sprite; + Vector2 drawOffset = outfitPreview.Sprites[j].drawOffset; float aspectRatio = outfitPreview.Dimensions.Y / outfitPreview.Dimensions.X; retVal[i][j] = new GUIImage(new RectTransform(new Vector2(0.7f / aspectRatio, 0.7f), innerFrame.RectTransform, Anchor.Center) - { RelativeOffset = sprite.Second / outfitPreview.Dimensions }, sprite.First, scaleToFit: true) + { RelativeOffset = drawOffset / outfitPreview.Dimensions }, sprite, scaleToFit: true) { PressedColor = Color.White, CanBeFocused = false diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 73e46e383..66ea4de08 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -1190,18 +1190,23 @@ 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 = ep.Description.IsNullOrEmpty() ? name : name + '\n' + ep.Description; + frame.ToolTip = $"{frame.ToolTip}\n‖color:{XMLExtensions.ToStringHex(GUIStyle.TextColorBright)}‖{name}‖color:end‖"; + if (!ep.Description.IsNullOrEmpty()) + { + frame.ToolTip += '\n' + ep.Description; + } if (ep.ContentPackage != GameMain.VanillaContent && ep.ContentPackage != null) { frame.Color = Color.Magenta; - frame.ToolTip = RichString.Rich($"{frame.ToolTip}\n‖color:{XMLExtensions.ToStringHex(Color.MediumPurple)}‖{ep.ContentPackage?.Name}‖color:end‖"); + frame.ToolTip = $"{frame.ToolTip}\n‖color:{XMLExtensions.ToStringHex(Color.MediumPurple)}‖{ep.ContentPackage?.Name}‖color:end‖"; } if (ep.HideInMenus) { frame.Color = Color.Red; name = "[HIDDEN] " + name; } + frame.ToolTip = RichString.Rich(frame.ToolTip); GUILayoutGroup paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.8f), frame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) { @@ -1976,7 +1981,7 @@ namespace Barotrauma return true; }; - nameBox.Text = subNameLabel?.Text?.SanitizedValue ?? ""; + nameBox.Text = MainSub?.Info.Name ?? ""; submarineNameCharacterCount.Text = nameBox.Text.Length + " / " + submarineNameLimit; @@ -2750,6 +2755,8 @@ namespace Barotrauma } } + nameBox.Text = nameBox.Text.Trim(); + bool hideInMenus = nameBox.Parent.GetChildByUserData("hideinmenus") is GUITickBox hideInMenusTickBox && hideInMenusTickBox.Selected; string saveFolder = Path.Combine(ContentPackage.LocalModsDir, nameBox.Text); string filePath = Path.Combine(saveFolder, $"{nameBox.Text}.xml").CleanUpPathCrossPlatform(); @@ -2884,8 +2891,30 @@ namespace Barotrauma { if (deleteButtonHolder.FindChild("delete") is GUIButton deleteBtn) { - deleteBtn.Enabled = userData is SubmarineInfo subInfo - && GetContentPackageIntrinsicallyTiedToSub(subInfo) != null; + deleteBtn.ToolTip = string.Empty; + if (!(userData is SubmarineInfo subInfo)) + { + deleteBtn.Enabled = false; + return true; + } + + var package = GetContentPackageIntrinsicallyTiedToSub(subInfo); + if (package != null) + { + deleteBtn.Enabled = true; + } + else + { + deleteBtn.Enabled = false; + if (ContentPackageManager.VanillaCorePackage?.Files.Any(f => f.Path == subInfo.FilePath) ?? false) + { + deleteBtn.ToolTip = TextManager.Get("cantdeletevanillasub"); + } + else if (ContentPackageManager.AllPackages.FirstOrDefault(p => p.Files.Any(f => f.Path == subInfo.FilePath)) is ContentPackage subPackage) + { + deleteBtn.ToolTip = TextManager.GetWithVariable("cantdeletemodsub", "[modname]", subPackage.Name); + } + } } return true; } @@ -2925,6 +2954,21 @@ namespace Barotrauma ToolTip = sub.FilePath }; + if (!(ContentPackageManager.VanillaCorePackage?.Files.Any(f => f.Path == sub.FilePath) ?? false)) + { + if (GetContentPackageIntrinsicallyTiedToSub(sub) == null && + ContentPackageManager.AllPackages.FirstOrDefault(p => p.Files.Any(f => f.Path == sub.FilePath)) is ContentPackage subPackage) + { + //workshop mod + textBlock.OverrideTextColor(Color.MediumPurple); + } + else + { + //local mod + textBlock.OverrideTextColor(GUIStyle.TextColorBright); + } + } + if (sub.HasTag(SubmarineTag.Shuttle)) { var shuttleText = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), textBlock.RectTransform, Anchor.CenterRight), @@ -3037,6 +3081,24 @@ namespace Barotrauma if (!(child.UserData is SubmarineInfo sub)) { continue; } child.Visible = string.IsNullOrEmpty(filter) || sub.Name.ToLower().Contains(filter.ToLower()); } + + //go through the elements backwards, and disable the labels for sub categories if there's no subs visible in them + bool subVisibleInCategory = false; + foreach (GUIComponent child in subList.Content.Children.Reverse()) + { + if (!(child.UserData is SubmarineInfo sub)) + { + if (child.Enabled) + { + child.Visible = subVisibleInCategory; + } + subVisibleInCategory = false; + } + else + { + subVisibleInCategory |= child.Visible; + } + } } /// @@ -4828,7 +4890,7 @@ namespace Barotrauma } } - if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.ToggleInventory].IsHit() && mode == Mode.Default) + if (PlayerInput.KeyHit(Keys.Q) && mode == Mode.Default) { toggleEntityMenuButton.OnClicked?.Invoke(toggleEntityMenuButton, toggleEntityMenuButton.UserData); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index c7d65c9ca..821725532 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -233,16 +233,18 @@ namespace Barotrauma float dist = diff.Length(); float distFallOff = dist / FlowSoundRange; - if (distFallOff >= 0.99f) continue; + if (distFallOff >= 0.99f) { continue; } + + float gain = MathHelper.Clamp(gapFlow / 100.0f, 0.0f, 1.0f); //flow at the left side if (diff.X < 0) { - targetFlowLeft[flowSoundIndex] += 1.0f - distFallOff; + targetFlowLeft[flowSoundIndex] += gain - distFallOff; } else { - targetFlowRight[flowSoundIndex] += 1.0f - distFallOff; + targetFlowRight[flowSoundIndex] += gain - distFallOff; } } } @@ -287,7 +289,7 @@ namespace Barotrauma flowSoundChannels[i] = FlowSounds[i].Sound.Play(1.0f, FlowSoundRange, soundPos); flowSoundChannels[i].Looping = true; } - flowSoundChannels[i].Gain = Math.Min(Math.Max(flowVolumeRight[i], flowVolumeLeft[i]), 1.0f); + flowSoundChannels[i].Gain = Math.Max(flowVolumeRight[i], flowVolumeLeft[i]); flowSoundChannels[i].Position = new Vector3(soundPos, 0.0f); } } @@ -853,7 +855,7 @@ namespace Barotrauma if (SplashSounds.Count == 0) { return; } int splashIndex = MathHelper.Clamp((int)(strength + Rand.Range(-2.0f, 2.0f)), 0, SplashSounds.Count - 1); float range = 800.0f; - var channel = SplashSounds[splashIndex].Sound.Play(1.0f, range, worldPosition, muffle: ShouldMuffleSound(Character.Controlled, worldPosition, range, null)); + SplashSounds[splashIndex].Sound?.Play(1.0f, range, worldPosition, muffle: ShouldMuffleSound(Character.Controlled, worldPosition, range, null)); } public static void PlayDamageSound(string damageType, float damage, PhysicsBody body) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index 6db48e03d..d4e1648a3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -39,9 +39,8 @@ namespace Barotrauma get { return texture != null && !cannotBeLoaded; } } - public Sprite(Sprite other) : this(other.texture, other.sourceRect, other.offset, other.rotation) + public Sprite(Sprite other) : this(other.texture, other.sourceRect, other.offset, other.rotation, other.FilePath.Value) { - FilePath = other.FilePath; Compress = other.Compress; size = other.size; effects = other.effects; @@ -58,6 +57,17 @@ namespace Barotrauma rotation = newRotation; FilePath = ContentPath.FromRaw(path); AddToList(this); + if (!string.IsNullOrEmpty(path)) + { + Identifier fullPath = Path.GetFullPath(path).CleanUpPathCrossPlatform(correctFilenameCase: false).ToIdentifier(); + lock (list) + { + if (!textureRefCounts.TryAdd(fullPath, new TextureRefCounter { RefCount = 1, Texture = texture })) + { + textureRefCounts[fullPath].RefCount++; + } + } + } } partial void LoadTexture(ref Vector4 sourceVector, ref bool shouldReturn) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index 37a04c2a0..fd9431cc1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -16,6 +16,8 @@ namespace Barotrauma.Steam { public static partial class Workshop { + public const int MaxThumbnailSize = 1024 * 1024; + public static readonly ImmutableArray Tags = new [] { "submarine", @@ -177,7 +179,7 @@ namespace Barotrauma.Steam DeletePublishStagingCopy(); Directory.CreateDirectory(PublishStagingDir); - await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, PublishStagingDir); + await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, PublishStagingDir, ShouldCorrectPaths.No); //Load filelist.xml and write the hash into it so anyone downloading this mod knows what it should be ModProject modProject = new ModProject(contentPackage) @@ -218,7 +220,7 @@ namespace Barotrauma.Steam throw new Exception($"{newPath} already exists"); } - await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, newPath); + await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, newPath, ShouldCorrectPaths.Yes); ModProject modProject = new ModProject(contentPackage); modProject.DiscardHashAndInstallTime(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs index ec55b9e72..fdc29524d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs @@ -197,6 +197,11 @@ namespace Barotrauma.Steam FileSelection.OnFileSelected = (fn) => { + if (new FileInfo(fn).Length > SteamManager.Workshop.MaxThumbnailSize) + { + new GUIMessageBox(TextManager.Get("Error"), TextManager.Get("WorkshopItemPreviewImageTooLarge")); + return; + } thumbnailPath = fn; CreateLocalThumbnail(thumbnailPath, thumbnailContainer); }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs index 692bc61b9..e803a86e2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs @@ -119,6 +119,11 @@ namespace Barotrauma.Steam { CanBeFocused = false }; + new GUICustomComponent(new RectTransform(Vector2.Zero, searchHolder.RectTransform), onUpdate: + (f, component) => + { + searchTitle.RectTransform.NonScaledSize = searchBox.Frame.RectTransform.NonScaledSize; + }); searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = searchBox.Text.IsNullOrWhiteSpace(); }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/LimitLString.cs b/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/LimitLString.cs index 8e652c5cd..804c688ca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/LimitLString.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/LimitLString.cs @@ -8,7 +8,6 @@ namespace Barotrauma private readonly int maxWidth; private ScalableFont? cachedFont = null; - private uint cachedFontSize = 0; public LimitLString(LocalizedString text, GUIFont font, int maxWidth) { @@ -27,7 +26,6 @@ namespace Barotrauma { cachedValue = ToolBox.LimitString(nestedStr.Value, font.Value, maxWidth); cachedFont = font.Value; - cachedFontSize = font.Size; UpdateLanguage(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs index 397c71876..c11566a30 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs @@ -404,7 +404,7 @@ namespace Barotrauma public static string LimitString(string str, ScalableFont font, int maxWidth) { - if (maxWidth <= 0 || string.IsNullOrWhiteSpace(str)) return ""; + if (maxWidth <= 0 || string.IsNullOrWhiteSpace(str)) { return ""; } float currWidth = font.MeasureString("...").X; for (int i = 0; i < str.Length; i++) diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index feb824001..a2ee9a9e0 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.15.0 + 0.18.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index c0b1d5d63..9360c73bf 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.15.0 + 0.18.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 870cafa2d..b9fecd89c 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.15.0 + 0.18.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index f494058e0..0f61a36a8 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.15.0 + 0.18.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index e3385b582..75305843b 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.15.0 + 0.18.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index a63d4e033..f2fde6748 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -37,7 +37,7 @@ namespace Barotrauma partial void OnPermanentStatChanged(StatTypes statType) { if (Character == null || Character.Removed) { return; } - GameMain.NetworkMember.CreateEntityEvent(Character, new Character.UpdatePermanentStatsEventData()); + GameMain.NetworkMember.CreateEntityEvent(Character, new Character.UpdatePermanentStatsEventData(statType)); } public void ServerWrite(IWriteMessage msg) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs index 541435d72..2c7358b04 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs @@ -25,9 +25,9 @@ namespace Barotrauma /// /// Saves bots in multiplayer /// - public void SaveMultiplayer(XElement root) + public XElement SaveMultiplayer(XElement parentElement) { - XElement saveElement = new XElement("bots", new XAttribute("hasbots", HasBots)); + var element = new XElement("bots", new XAttribute("hasbots", HasBots)); foreach (CharacterInfo info in characterInfos) { if (Level.Loaded != null) @@ -35,13 +35,13 @@ namespace Barotrauma if (!info.IsNewHire && (info.Character == null || info.Character.IsDead)) { continue; } } - XElement characterElement = info.Save(saveElement); + XElement characterElement = info.Save(element); if (info.InventoryData != null) { characterElement.Add(info.InventoryData); } if (info.HealthData != null) { characterElement.Add(info.HealthData); } if (info.OrderData != null) { characterElement.Add(info.OrderData); } } - SaveActiveOrders(saveElement); - root.Add(saveElement); + parentElement?.Add(element); + return element; } public void ServerWriteActiveOrders(IWriteMessage msg) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs index a2bfc7450..9360c71d2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; namespace Barotrauma { @@ -10,6 +11,31 @@ namespace Barotrauma protected set; } + private static bool IsOwner(Client client) => client != null && client.Connection == GameMain.Server.OwnerConnection; + + /// + /// There is a client-side implementation of the method in + /// + public bool AllowedToManageCampaign(Client client, ClientPermissions permissions) + { + //allow managing the campaign if the client has permissions, is the owner, or the only client in the server, + //or if no-one has management permissions + return + client.HasPermission(permissions) || + client.HasPermission(ClientPermissions.ManageCampaign) || + GameMain.Server.ConnectedClients.Count == 1 || + IsOwner(client) || + GameMain.Server.ConnectedClients.None(c => c.InGame && (IsOwner(c) || c.HasPermission(permissions))); + } + + public bool AllowedToManageWallets(Client client) + { + return + client.HasPermission(ClientPermissions.ManageCampaign) || + client.HasPermission(ClientPermissions.ManageMoney) || + IsOwner(client); + } + public override void ShowStartMessage() { foreach (Mission mission in Missions) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 965530ee9..858cb5b29 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -163,29 +163,6 @@ namespace Barotrauma private static bool IsOwner(Client client) => client != null && client.Connection == GameMain.Server.OwnerConnection; - /// - /// There is a client-side implementation of the method in - /// - public bool AllowedToManageCampaign(Client client, ClientPermissions permissions) - { - //allow managing the campaign if the client has permissions, is the owner, or the only client in the server, - //or if no-one has management permissions - return - client.HasPermission(permissions) || - client.HasPermission(ClientPermissions.ManageCampaign) || - GameMain.Server.ConnectedClients.Count == 1 || - IsOwner(client) || - GameMain.Server.ConnectedClients.None(c => c.InGame && (IsOwner(c) || c.HasPermission(permissions))); - } - - public bool AllowedToManageWallets(Client client) - { - return - client.HasPermission(ClientPermissions.ManageCampaign) || - client.HasPermission(ClientPermissions.ManageMoney) || - IsOwner(client); - } - public void SaveExperiencePoints(Client client) { ClearSavedExperiencePoints(client); @@ -200,14 +177,6 @@ namespace Barotrauma savedExperiencePoints.RemoveAll(s => s.SteamID != 0 && client.SteamID == s.SteamID || client.EndpointMatches(s.EndPoint)); } - public void LoadPets() - { - if (petsElement != null) - { - PetBehavior.LoadPets(petsElement); - } - } - public void SavePlayers() { //refresh the character data of clients who are still in the server @@ -261,8 +230,7 @@ namespace Barotrauma characterData.ForEach(cd => cd.HasSpawned = false); - petsElement = new XElement("pets"); - PetBehavior.SavePets(petsElement); + SavePets(); //remove all items that are in someone's inventory foreach (Character c in Character.CharacterList) @@ -285,6 +253,8 @@ namespace Barotrauma c.Inventory.DeleteAllItems(); } + + SaveActiveOrders(); } public void MoveDiscardedCharacterBalancesToBank() @@ -348,44 +318,10 @@ namespace Barotrauma if (success) { SavePlayers(); - yield return CoroutineStatus.Running; - - if (leavingSub != Submarine.MainSub && !leavingSub.DockedTo.Contains(Submarine.MainSub)) - { - Submarine.MainSub = leavingSub; - GameMain.GameSession.Submarine = leavingSub; - GameMain.GameSession.SubmarineInfo = leavingSub.Info; - leavingSub.Info.FilePath = System.IO.Path.Combine(SaveUtil.TempPath, leavingSub.Info.Name + ".sub"); - var subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub); - GameMain.GameSession.OwnedSubmarines.Add(leavingSub.Info); - foreach (Submarine sub in subsToLeaveBehind) - { - GameMain.GameSession.OwnedSubmarines.RemoveAll(s => s != leavingSub.Info && s.Name == sub.Info.Name); - MapEntity.mapEntityList.RemoveAll(e => e.Submarine == sub && e is LinkedSubmarine); - LinkedSubmarine.CreateDummy(leavingSub, sub); - } - } + LeaveUnconnectedSubs(leavingSub); NextLevel = newLevel; - GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - - if (PendingSubmarineSwitch != null) - { - SubmarineInfo previousSub = GameMain.GameSession.SubmarineInfo; - GameMain.GameSession.SubmarineInfo = PendingSubmarineSwitch; - - for (int i = 0; i < GameMain.GameSession.OwnedSubmarines.Count; i++) - { - if (GameMain.GameSession.OwnedSubmarines[i].Name == previousSub.Name) - { - GameMain.GameSession.OwnedSubmarines[i] = previousSub; - break; - } - } - } - SaveUtil.SaveGame(GameMain.GameSession.SavePath); - PendingSubmarineSwitch = null; } else { @@ -977,6 +913,8 @@ namespace Barotrauma { NetWalletTransfer transfer = INetSerializableStruct.Read(msg); + if (GameMain.Server is null) { return; } + switch (transfer.Sender) { case Some { Value: var id }: @@ -992,7 +930,8 @@ namespace Barotrauma { if (transfer.Receiver is Some { Value: var receiverId } && receiverId == sender.CharacterID) { - GameMain.Server?.Voting.StartTransferVote(sender, null, transfer.Amount, sender); + if (transfer.Amount > GameMain.Server.ServerSettings.MaximumTransferRequest) { return; } + GameMain.Server.Voting.StartTransferVote(sender, null, transfer.Amount, sender); GameServer.Log($"{sender.Name} started a vote to transfer {transfer.Amount} mk from the bank.", ServerLog.MessageType.Money); } return; @@ -1301,7 +1240,11 @@ namespace Barotrauma } // save bots - CrewManager.SaveMultiplayer(modeElement); + var crewManagerElement = CrewManager.SaveMultiplayer(modeElement); + if (ActiveOrdersElement != null) + { + crewManagerElement.Add(ActiveOrdersElement); + } XElement savedExperiencePointsElement = new XElement("SavedExperiencePoints"); foreach (var savedExperiencePoint in savedExperiencePoints) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs index aa7abb66a..607149e26 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs @@ -1,19 +1,27 @@ using Barotrauma.Networking; -using System; namespace Barotrauma.Items.Components { - partial class DockingPort : ItemComponent, IDrawableComponent, IServerSerializable + partial class DockingPort : ItemComponent, IDrawableComponent, IServerSerializable, IClientSerializable { public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(docked); - if (docked) { msg.Write(DockingTarget.item.ID); msg.Write(IsLocked); } } + public void ServerEventRead(IReadMessage msg, Client c) + { + var allowOutpostAutoDocking = (AllowOutpostAutoDocking)msg.ReadByte(); + if (outpostAutoDockingPromptShown && + (GameMain.GameSession?.Campaign?.AllowedToManageCampaign(c, ClientPermissions.ManageMap) ?? false)) + { + this.allowOutpostAutoDocking = allowOutpostAutoDocking; + } + } + } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs index 063f23d7a..ad7dd525f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs @@ -39,7 +39,7 @@ namespace Barotrauma.Items.Components set; } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { //do nothing } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs index 2e27864a2..a9cffe9ef 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs @@ -15,7 +15,8 @@ namespace Barotrauma.Items.Components for (int i = 0; i < Connections.Count; i++) { wires[i] = new List(); - for (int j = 0; j < Connections[i].MaxWires; j++) + uint wireCount = msg.ReadVariableUInt32(); + for (int j = 0; j < wireCount; j++) { ushort wireId = msg.ReadUInt16(); @@ -91,12 +92,8 @@ namespace Barotrauma.Items.Components //go through existing wire links for (int i = 0; i < Connections.Count; i++) { - int j = -1; - foreach (Wire existingWire in Connections[i].Wires) + foreach (Wire existingWire in Connections[i].Wires.ToArray()) { - j++; - if (existingWire == null) { continue; } - //existing wire not in the list of new wires -> disconnect it if (!wires[i].Contains(existingWire)) { @@ -163,7 +160,7 @@ namespace Barotrauma.Items.Components }*/ } - Connections[i].SetWire(j, null); + Connections[i].DisconnectWire(existingWire); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs index 9645311ce..877d0b4b1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs @@ -42,7 +42,7 @@ namespace Barotrauma.MapCreatures.Behavior foreach (BallastFloraBranch branch in Branches) { //don't notify about minuscule amounts of damage (<= 1.0f) - if (branch.AccumulatedDamage > 1.0f) + if (Math.Abs(branch.AccumulatedDamage) > 1.0f) { CreateNetworkMessage(new BranchDamageEventData(branch)); branch.AccumulatedDamage = 0.0f; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index 5a31dc4cf..ea54be031 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -24,7 +24,7 @@ namespace Barotrauma.Networking public UInt16 LastRecvCampaignUpdate = 0; public UInt16 LastRecvCampaignSave = 0; - public Pair LastCampaignSaveSendTime; + public (UInt16 saveId, float time) LastCampaignSaveSendTime; public readonly List ChatMsgQueue = new List(); public UInt16 LastChatMsgQueueID; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs index 7d5385f7f..b79b94e64 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs @@ -391,7 +391,7 @@ namespace Barotrauma.Networking StartTransfer(inc.Sender, FileTransferType.CampaignSave, GameMain.GameSession.SavePath); if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) { - client.LastCampaignSaveSendTime = new Pair(campaign.LastSaveID, (float)Lidgren.Network.NetTime.Now); + client.LastCampaignSaveSendTime = (campaign.LastSaveID, (float)Lidgren.Network.NetTime.Now); } } break; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 28d5adb13..effeb21a5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -955,7 +955,9 @@ namespace Barotrauma.Networking } if (Level.Loaded != null) { - errorLines.Add("Level: " + Level.Loaded.Seed + ", " + string.Join(", ", Level.Loaded.EqualityCheckValues.Select(cv => cv.ToString("X")))); + errorLines.Add("Level: " + Level.Loaded.Seed + ", " + + string.Join("; ", Level.Loaded.EqualityCheckValues.Select(cv + => cv.Key + "=" + cv.Value.ToString("X")))); errorLines.Add("Entity count before generating level: " + Level.Loaded.EntityCountBeforeGenerate); errorLines.Add("Entities:"); foreach (Entity e in Level.Loaded.EntitiesBeforeGenerate.OrderBy(e => e.CreationIndex)) @@ -1548,11 +1550,11 @@ namespace Barotrauma.Networking NetIdUtils.IdMoreRecent(campaign.LastSaveID, c.LastRecvCampaignSave)) { //already sent an up-to-date campaign save - if (c.LastCampaignSaveSendTime != null && campaign.LastSaveID == c.LastCampaignSaveSendTime.First) + if (c.LastCampaignSaveSendTime != default && campaign.LastSaveID == c.LastCampaignSaveSendTime.saveId) { //the save was sent less than 5 second ago, don't attempt to resend yet //(the client may have received it but hasn't acked us yet) - if (c.LastCampaignSaveSendTime.Second > NetTime.Now - 5.0f) + if (c.LastCampaignSaveSendTime.time > NetTime.Now - 5.0f) { return; } @@ -1561,7 +1563,7 @@ namespace Barotrauma.Networking if (!FileSender.ActiveTransfers.Any(t => t.Connection == c.Connection && t.FileType == FileTransferType.CampaignSave)) { FileSender.StartTransfer(c.Connection, FileTransferType.CampaignSave, GameMain.GameSession.SavePath); - c.LastCampaignSaveSendTime = new Pair(campaign.LastSaveID, (float)NetTime.Now); + c.LastCampaignSaveSendTime = (campaign.LastSaveID, (float)NetTime.Now); } } } @@ -2193,7 +2195,7 @@ namespace Barotrauma.Networking Level.Loaded?.SpawnNPCs(); Level.Loaded?.SpawnCorpses(); Level.Loaded?.PrepareBeaconStation(); - AutoItemPlacer.PlaceIfNeeded(); + AutoItemPlacer.SpawnItems(); CrewManager crewManager = campaign?.CrewManager; @@ -2388,7 +2390,9 @@ namespace Barotrauma.Networking } campaign?.LoadPets(); - crewManager?.LoadActiveOrders(); + campaign?.LoadActiveOrders(); + + campaign?.CargoManager.InitPurchasedIDCards(); foreach (Submarine sub in Submarine.MainSubs) { @@ -2400,7 +2404,7 @@ namespace Barotrauma.Networking spawnList.Add(new PurchasedItem(kvp.Key, kvp.Value, buyer: null)); } - CargoManager.CreateItems(spawnList, sub); + CargoManager.CreateItems(spawnList, sub, cargoManager: null); } TraitorManager = null; @@ -2531,10 +2535,9 @@ namespace Barotrauma.Networking { msg.Write(mission.Prefab.Identifier); } - msg.Write((byte)GameMain.GameSession.Level.EqualityCheckValues.Count); - foreach (int equalityCheckValue in GameMain.GameSession.Level.EqualityCheckValues) + foreach (Level.LevelGenStage stage in Enum.GetValues(typeof(Level.LevelGenStage)).OfType().OrderBy(s => s)) { - msg.Write(equalityCheckValue); + msg.Write(GameMain.GameSession.Level.EqualityCheckValues[stage]); } foreach (Mission mission in GameMain.GameSession.Missions) { @@ -3178,9 +3181,9 @@ namespace Barotrauma.Networking Client recipient = connectedClients.Find(c => c.Connection == transfer.Connection); if (transfer.FileType == FileTransferType.CampaignSave && (transfer.Status == FileTransferStatus.Sending || transfer.Status == FileTransferStatus.Finished) && - recipient.LastCampaignSaveSendTime != null) + recipient.LastCampaignSaveSendTime != default) { - recipient.LastCampaignSaveSendTime.Second = (float)Lidgren.Network.NetTime.Now; + recipient.LastCampaignSaveSendTime.time = (float)NetTime.Now; } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs index 8fe44eb71..e86a858f5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs @@ -278,12 +278,12 @@ namespace Barotrauma static bool isValid(Item item) { - return item.Prefab.Identifier == "idcard" || item.GetComponent() != null || item.GetComponent() != null; + return item.GetComponent() != null || item.GetComponent() != null || item.GetComponent() != null; } if (foundItem == null) { return; } - bool isIdCard = ((MapEntity)foundItem).Prefab.Identifier == "idcard"; + bool isIdCard = foundItem.GetComponent() != null; bool isWeapon = foundItem.GetComponent() != null || foundItem.GetComponent() != null; if (isIdCard) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index b11700a19..28d1b91c0 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -441,32 +441,43 @@ namespace Barotrauma.Networking GameServer.Log(string.Format("Respawning {0} ({1}) as {2}", GameServer.ClientLogName(clients[i]), clients[i].Connection?.EndPointString, characterInfos[i].Job.Name), ServerLog.MessageType.Spawning); } - if (divingSuitPrefab != null && oxyPrefab != null && RespawnShuttle != null) + if (RespawnShuttle != null) { Vector2 pos = cargoSp == null ? character.Position : cargoSp.Position; - if (divingSuitPrefab != null && oxyPrefab != null) + if (divingSuitPrefab != null) { var divingSuit = new Item(divingSuitPrefab, pos, respawnSub); Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(divingSuit)); respawnItems.Add(divingSuit); - var oxyTank = new Item(oxyPrefab, pos, respawnSub); - Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(oxyTank)); - divingSuit.Combine(oxyTank, user: null); - respawnItems.Add(oxyTank); + if (oxyPrefab != null && divingSuit.GetComponent() != null) + { + var oxyTank = new Item(oxyPrefab, pos, respawnSub); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(oxyTank)); + divingSuit.Combine(oxyTank, user: null); + respawnItems.Add(oxyTank); + } } - if (scooterPrefab != null && batteryPrefab != null) + if (!(GameMain.GameSession.GameMode is CampaignMode)) { - var scooter = new Item(scooterPrefab, pos, respawnSub); - Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(scooter)); - - var battery = new Item(batteryPrefab, pos, respawnSub); - Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(battery)); - - scooter.Combine(battery, user: null); - respawnItems.Add(scooter); - respawnItems.Add(battery); + if (scooterPrefab != null) + { + var scooter = new Item(scooterPrefab, pos, respawnSub); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(scooter)); + respawnItems.Add(scooter); + if (batteryPrefab != null) + { + var battery = new Item(batteryPrefab, pos, respawnSub); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(battery)); + scooter.Combine(battery, user: null); + respawnItems.Add(battery); + } + } + } + if (respawnContainer != null) + { + AutoItemPlacer.RegenerateLoot(RespawnShuttle, respawnContainer); } } @@ -504,7 +515,7 @@ namespace Barotrauma.Networking //add the ID card tags they should've gotten when spawning in the shuttle foreach (Item item in character.Inventory.AllItems.Distinct()) { - if (item.Prefab.Identifier != "idcard") { continue; } + if (item.GetComponent() == null) { continue; } foreach (string s in shuttleSpawnPoints[i].IdCardTags) { item.AddTag(s); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index f8a129f51..12fa0d853 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -44,12 +44,14 @@ namespace Barotrauma { GameMain.Server?.SwitchSubmarine(); } + else + { + voting.RegisterRejectedVote(this); + } voting.StopSubmarineVote(passed); } } - public static IVote ActiveVote; - public class TransferVote : IVote { public Client VoteStarter { get; } @@ -83,12 +85,22 @@ namespace Barotrauma toWallet.Give(TransferAmount); } } + else + { + voting.RegisterRejectedVote(this); + } voting.StopMoneyTransferVote(passed); } } + public static IVote ActiveVote; + private static readonly Queue pendingVotes = new Queue(); + private readonly TimeSpan rejectedVoteCooldown = new TimeSpan(0, 1, 0); + + private readonly Dictionary rejectedVoteTimes = new Dictionary(); + private void StartSubmarineVote(SubmarineInfo subInfo, VoteType voteType, Client sender) { if (ActiveVote == null) @@ -136,6 +148,10 @@ namespace Barotrauma public void StartTransferVote(Client starter, Client from, int transferAmount, Client to) { + if (ShouldRejectVote(starter, VoteType.TransferMoney)) + { + return; + } if (ActiveVote == null) { starter.SetVote(VoteType.TransferMoney, 2); @@ -156,6 +172,31 @@ namespace Barotrauma } } + private bool ShouldRejectVote(Client sender, VoteType voteType) + { + if (rejectedVoteTimes.ContainsKey(sender)) + { + TimeSpan remainingCooldown = (rejectedVoteTimes[sender].time + rejectedVoteCooldown) - DateTime.Now; + if (rejectedVoteTimes[sender].voteType == voteType && + remainingCooldown.TotalSeconds > 0) + { + GameMain.Server.SendDirectChatMessage( + TextManager.FormatServerMessage("voterejectedpleasewait", ("[time]", ((int)remainingCooldown.TotalSeconds).ToString())), + sender, ChatMessageType.ServerMessageBox); + return true; + } + } + return false; + } + + protected void RegisterRejectedVote(IVote vote) + { + if (vote.VoteStarter != null) + { + rejectedVoteTimes[vote.VoteStarter] = (vote.VoteType, DateTime.Now); + } + } + public void Update(float deltaTime) { if (ActiveVote == null) { return; } @@ -227,7 +268,6 @@ namespace Barotrauma GameServer.Log(GameServer.ClientLogName(sender) + (ready ? " is ready to start the game." : " is not ready to start the game."), ServerLog.MessageType.ServerMessage); } break; - case VoteType.PurchaseAndSwitchSub: case VoteType.PurchaseSub: case VoteType.SwitchSub: @@ -240,18 +280,25 @@ namespace Barotrauma int amount = inc.ReadInt32(); int fromClientId = inc.ReadByte(); int toClientId = inc.ReadByte(); - pendingVotes.Enqueue(new TransferVote(sender, - GameMain.Server.ConnectedClients.Find(c => c.ID == fromClientId), - amount, - GameMain.Server.ConnectedClients.Find(c => c.ID == toClientId))); + if (!ShouldRejectVote(sender, voteType)) + { + pendingVotes.Enqueue(new TransferVote(sender, + GameMain.Server.ConnectedClients.Find(c => c.ID == fromClientId), + amount, + GameMain.Server.ConnectedClients.Find(c => c.ID == toClientId))); + } } else { string subName = inc.ReadString(); SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName); - if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign && (campaign.CanPurchaseSub(subInfo, sender) || GameMain.GameSession.IsSubmarineOwned(subInfo))) + if (!ShouldRejectVote(sender, voteType)) { - StartSubmarineVote(subInfo, voteType, sender); + if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign && + (campaign.CanPurchaseSub(subInfo, sender) || GameMain.GameSession.IsSubmarineOwned(subInfo))) + { + StartSubmarineVote(subInfo, voteType, sender); + } } } } diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 54c93dda6..c5f7034cc 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.15.0 + 0.18.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index 9c942ee08..cc9a9100e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -31,6 +31,17 @@ namespace Barotrauma if (_previousAiTarget != null) { _lastAiTarget = _previousAiTarget; + if (_selectedAiTarget != null) + { + if (_selectedAiTarget.Entity is Item i && _previousAiTarget.Entity is Character c) + { + if (i.IsOwnedBy(c)) { return; } + } + else if (_previousAiTarget.Entity is Item it && _selectedAiTarget.Entity is Character ch) + { + if (it.IsOwnedBy(ch)) { return; } + } + } } OnTargetChanged(_previousAiTarget, _selectedAiTarget); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 0edb7fa1d..8bc57ffa0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -1055,6 +1055,9 @@ namespace Barotrauma private Vector2 attackWorldPos; private Vector2 attackSimPos; + private float reachTimer; + // 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 void UpdateAttack(float deltaTime) { @@ -1427,6 +1430,22 @@ namespace Barotrauma // Check that we can reach the target distance = toTarget.Length(); canAttack = distance < AttackLimb.attack.Range; + if (canAttack) + { + reachTimer = 0; + } + else if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && distance < AttackLimb.attack.Range * 5) + { + reachTimer += deltaTime; + if (reachTimer > reachTimeOut) + { + reachTimer = 0; + IgnoreTarget(SelectedAiTarget); + State = AIState.Idle; + ResetAITarget(); + return; + } + } // Crouch if the target is down (only humanoids), so that we can reach it. if (Character.AnimController is HumanoidAnimController humanoidAnimController && distance < AttackLimb.attack.Range * 2) @@ -1958,9 +1977,8 @@ namespace Barotrauma } if (!isFriendly && attackResult.Damage > 0.0f) { - ignoredTargets.Remove(attacker.AiTarget); bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackWalls; - if (AIParams.AttackWhenProvoked && canAttack) + if (AIParams.AttackWhenProvoked && canAttack && !ignoredTargets.Contains(attacker.AiTarget)) { if (attacker.IsHusk) { @@ -3476,6 +3494,7 @@ namespace Barotrauma { observeTimer = targetParams.Timer * Rand.Range(0.75f, 1.25f); } + reachTimer = 0; } protected override void OnStateChanged(AIState from, AIState to) @@ -3496,6 +3515,7 @@ namespace Barotrauma SetStateResetTimer(); } blockCheckTimer = 0; + reachTimer = 0; } private void SetStateResetTimer() => stateResetTimer = stateResetCooldown * Rand.Range(0.75f, 1.25f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 651c3bbf9..a63a5e0ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -59,7 +59,11 @@ namespace Barotrauma private readonly float enemyCheckInterval = 0.2f; private readonly float enemySpotDistanceOutside = 800; private readonly float enemySpotDistanceInside = 1000; - private float enemycheckTimer; + private float enemyCheckTimer; + + private readonly float reportProblemsInterval = 1.0f; + private float reportProblemsTimer; + /// /// How far other characters can hear reports done by this character (e.g. reports for fires, intruders). Defaults to infinity. @@ -166,6 +170,7 @@ namespace Barotrauma objectiveManager = new AIObjectiveManager(c); reactTimer = GetReactionTime(); SortTimer = Rand.Range(0f, sortObjectiveInterval); + reportProblemsTimer = Rand.Range(0f, reportProblemsInterval); } public override void Update(float deltaTime) @@ -309,10 +314,10 @@ namespace Barotrauma { // Spot enemies while staying outside or inside an enemy ship. // does not apply for escorted characters, such as prisoners or terrorists who have their own behavior - enemycheckTimer -= deltaTime; - if (enemycheckTimer < 0) + enemyCheckTimer -= deltaTime; + if (enemyCheckTimer < 0) { - enemycheckTimer = enemyCheckInterval * Rand.Range(0.75f, 1.25f); + enemyCheckTimer = enemyCheckInterval * Rand.Range(0.75f, 1.25f); if (!objectiveManager.IsCurrentObjective()) { float closestDistance = 0; @@ -407,19 +412,29 @@ namespace Barotrauma { if (Character.IsOnPlayerTeam) { - VisibleHulls.ForEach(h => PropagateHullSafety(Character, h)); + foreach (Hull h in VisibleHulls) + { + PropagateHullSafety(Character, h); + } } else { - // Outpost npcs don't inform each other about threats, like crew members do. - VisibleHulls.ForEach(h => RefreshHullSafety(h)); + foreach (Hull h in VisibleHulls) + { + RefreshHullSafety(h); + } } } if (Character.SpeechImpediment < 100.0f) { - if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck) + reportProblemsTimer -= deltaTime; + if (reportProblemsTimer <= 0.0f) { - ReportProblems(); + if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck) + { + ReportProblems(); + } + reportProblemsTimer = reportProblemsInterval; } UpdateSpeaking(); } @@ -785,9 +800,10 @@ namespace Barotrauma if (item == null || item.Removed) { return; } if (!itemsToRelocate.Contains(item)) { return; } var mainSub = Submarine.MainSub; - if (item.ParentInventory != null) + Entity owner = item.GetRootInventoryOwner(); + if (owner != null) { - if (item.ParentInventory.Owner is Character c) + if (owner is Character c) { if (c.TeamID == CharacterTeamType.Team1 || c.TeamID == CharacterTeamType.Team2) { @@ -795,24 +811,37 @@ namespace Barotrauma return; } } - else if (item.ParentInventory.Owner.Submarine == mainSub) + else if (owner.Submarine == mainSub) { // Placed inside an inventory that's already in the main sub. return; } } - // Laying on ground inside the main sub. + // Laying on the ground inside the main sub. if (item.Submarine == mainSub) { return; } - WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, mainSub); - if (wp != null) + if (owner != null && owner != item) { - item.Submarine = mainSub; - item.SetTransform(wp.SimPosition, 0.0f); + item.Drop(null); + } + item.Submarine = mainSub; + Item newContainer = mainSub.FindContainerFor(item, onlyPrimary: false); + if (newContainer == null || !newContainer.OwnInventory.TryPutItem(item, user: null)) + { + WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, mainSub) ?? WayPoint.GetRandom(SpawnType.Path, null, mainSub); + if (wp != null) + { + item.SetTransform(wp.SimPosition, 0.0f, findNewHull: false, setPrevTransform: false); + } + else + { + DebugConsole.AddWarning($"Failed to relocate item {item.Prefab.Identifier} ({item.ID}), because no cargo spawn point could be found!"); + } } itemsToRelocate.Remove(item); + DebugConsole.Log($"Relocated item {item.Prefab.Identifier} ({item.ID}) back to the main sub."); } } @@ -1149,7 +1178,7 @@ namespace Barotrauma bool isAttackerFightingEnemy = false; float minorDamageThreshold = 1; float majorDamageThreshold = 20; - if (attacker.TeamID == Character.TeamID) + if (attacker.TeamID == Character.TeamID && !attacker.IsInstigator) { minorDamageThreshold = 10; majorDamageThreshold = 40; @@ -1356,6 +1385,10 @@ namespace Barotrauma Character FindInstigator() { + if (Character.IsInstigator) + { + return Character; + } if (attacker.IsInstigator) { return attacker; @@ -1545,7 +1578,7 @@ namespace Barotrauma (!requireEquipped || character.HasEquippedItem(i)) && (predicate == null || predicate(i)), recursive, matchingItems); items = matchingItems; - return matchingItems.Any(i => i != null && (containedTag.IsEmpty || i.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage))); + return matchingItems.Any(i => i != null && (containedTag.IsEmpty || i.OwnInventory == null || i.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage))); } public static void StructureDamaged(Structure structure, float damageAmount, Character character) @@ -1889,7 +1922,7 @@ namespace Barotrauma float fireFactor = 1; if (!ignoreFire) { - float calculateFire(Hull h) => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X; + static float calculateFire(Hull h) => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X; // Even the smallest fire reduces the safety by 50% float fire = visibleHulls == null ? calculateFire(hull) : visibleHulls.Sum(h => calculateFire(h)); fireFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(fire, 0, 1)); @@ -1897,10 +1930,22 @@ namespace Barotrauma float enemyFactor = 1; if (!ignoreEnemies) { - bool isValidTarget(Character e) => IsActive(e) && !IsFriendly(character, e) && !e.IsArrested; - int enemyCount = visibleHulls == null ? - Character.CharacterList.Count(e => isValidTarget(e) && e.CurrentHull == hull) : - Character.CharacterList.Count(e => isValidTarget(e) && visibleHulls.Contains(e.CurrentHull)); + int enemyCount = 0; + foreach (Character c in Character.CharacterList) + { + if (visibleHulls == null) + { + if (c.CurrentHull != hull) { continue; } + } + else + { + if (!visibleHulls.Contains(c.CurrentHull)) { continue; } + } + if (IsActive(c) && !IsFriendly(character, c) && !c.IsArrested) + { + enemyCount++; + } + } // The hull safety decreases 90% per enemy up to 100% (TODO: test smaller percentages) enemyFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(enemyCount * 0.9f, 0, 1)); } @@ -1911,6 +1956,7 @@ namespace Barotrauma if (item.Prefab != null && item.Prefab.IsDangerous) { dangerousItemsFactor = 0; + break; } } float safety = oxygenFactor * waterFactor * fireFactor * enemyFactor * dangerousItemsFactor; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index c647b82d0..9222801ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -113,7 +113,7 @@ namespace Barotrauma else { var connectionPanel = item.GetComponent(); - if (connectionPanel != null && connectionPanel.Connections.Any(c => c.Wires.Any(w => w != null))) + if (connectionPanel != null && connectionPanel.Connections.Any(c => c.Wires.Count > 0)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index bdc673490..c3a21854d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using FarseerPhysics.Dynamics; +using static Barotrauma.AIObjectiveFindSafety; namespace Barotrauma { @@ -775,7 +776,13 @@ namespace Barotrauma } else { - retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls, allowChangingTheSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC); + HullSearchStatus hullSearchStatus = findSafety.FindBestHull(out Hull potentialSafeHull, HumanAIController.VisibleHulls, allowChangingSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC); + if (hullSearchStatus != HullSearchStatus.Finished) + { + findSafety.UpdateSimpleEscape(deltaTime); + return; + } + retreatTarget = potentialSafeHull; findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f); } } @@ -785,21 +792,21 @@ namespace Barotrauma { UsePathingOutside = false }, - onAbandon: () => + onAbandon: () => + { + if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull)) { - if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull)) - { - // If in the same room with an enemy -> don't try to escape because we'd want to fight it - SteeringManager.Reset(); - RemoveSubObjective(ref retreatObjective); - } - else - { - // else abandon and fall back to find safety mode - Abandon = true; - } - }, - onCompleted: () => RemoveSubObjective(ref retreatObjective)); + // If in the same room with an enemy -> don't try to escape because we'd want to fight it + SteeringManager.Reset(); + RemoveSubObjective(ref retreatObjective); + } + else + { + // else abandon and fall back to find safety mode + Abandon = true; + } + }, + onCompleted: () => RemoveSubObjective(ref retreatObjective)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index f3380c63a..2eb5b453e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -1,4 +1,5 @@ -using FarseerPhysics; +using Barotrauma.Extensions; +using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -192,9 +193,17 @@ namespace Barotrauma } else { + HullSearchStatus hullSearchStatus = FindBestHull(out Hull potentialSafeHull, allowChangingSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC); + if (hullSearchStatus != HullSearchStatus.Finished) + { + UpdateSimpleEscape(deltaTime); + return; + } + searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f); previousSafeHull = currentSafeHull; - currentSafeHull = FindBestHull(allowChangingTheSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC); + currentSafeHull = potentialSafeHull; + cannotFindSafeHull = currentSafeHull == null || HumanAIController.NeedsDivingGear(currentSafeHull, out _); if (currentSafeHull == null) { @@ -250,58 +259,122 @@ namespace Barotrauma } } if (subObjectives.Any(so => so.CanBeCompleted)) { return; } - if (currentHull != null) + UpdateSimpleEscape(deltaTime); + } + } + + public void UpdateSimpleEscape(float deltaTime) + { + Vector2 escapeVel = Vector2.Zero; + if (character.CurrentHull != null) + { + foreach (Hull hull in HumanAIController.VisibleHulls) { - //goto objective doesn't exist (a safe hull not found, or a path to a safe hull not found) - // -> attempt to manually steer away from hazards - Vector2 escapeVel = Vector2.Zero; - foreach (Hull hull in HumanAIController.VisibleHulls) + foreach (FireSource fireSource in hull.FireSources) { - foreach (FireSource fireSource in hull.FireSources) - { - Vector2 dir = character.Position - fireSource.Position; - float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f); - escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier); - } - } - foreach (Character enemy in Character.CharacterList) - { - if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsArrested) { continue; } - if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull)) - { - Vector2 dir = character.Position - enemy.Position; - float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f); - escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier); - } - } - if (escapeVel != Vector2.Zero) - { - float left = currentHull.Rect.X + 50; - float right = currentHull.Rect.Right - 50; - //only move if we haven't reached the edge of the room - if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right) - { - character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel); - } - else - { - character.AnimController.TargetDir = escapeVel.X < 0.0f ? Direction.Right : Direction.Left; - character.AIController.SteeringManager.Reset(); - } - return; + Vector2 dir = character.Position - fireSource.Position; + float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f); + escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier); } } + foreach (Character enemy in Character.CharacterList) + { + if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsArrested) { continue; } + if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull)) + { + Vector2 dir = character.Position - enemy.Position; + float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f); + escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier); + } + } + } + if (escapeVel != Vector2.Zero) + { + float left = character.CurrentHull.Rect.X + 50; + float right = character.CurrentHull.Rect.Right - 50; + //only move if we haven't reached the edge of the room + if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right) + { + character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel); + } + else + { + character.AnimController.TargetDir = escapeVel.X < 0.0f ? Direction.Right : Direction.Left; + character.AIController.SteeringManager.Reset(); + } + } + else + { objectiveManager.GetObjective().Wander(deltaTime); } } - public Hull FindBestHull(IEnumerable ignoredHulls = null, bool allowChangingTheSubmarine = true) + public enum HullSearchStatus { - //sort the hulls based on distance and which sub they're in - //tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily - //(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive - //path calculations, only to discard all of them when going through the hulls in the outpost) - float EstimateHullSuitability(Hull hull) + Running, + Finished + } + + private readonly List hulls = new List(); + private int hullSearchIndex = -1; + float bestHullValue = 0; + bool bestHullIsAirlock = false; + Hull potentialBestHull; + + /// + /// Tries to find the best (safe, nearby) hull the character can find a path to. + /// Checks one hull at a time, and returns HullSearchStatus.Finished when all potential hulls have been checked. + /// + public HullSearchStatus FindBestHull(out Hull bestHull, IEnumerable ignoredHulls = null, bool allowChangingSubmarine = true) + { + if (hullSearchIndex == -1) + { + bestHullValue = 0; + potentialBestHull = null; + bestHullIsAirlock = false; + hulls.Clear(); + var connectedSubs = character.Submarine?.GetConnectedSubs(); + foreach (Hull hull in Hull.HullList) + { + if (hull.Submarine == null) { continue; } + // Ruins are mazes filled with water. There's no safe hulls and we don't want to use the resources on it. + if (hull.Submarine.Info.IsRuin) { continue; } + if (!allowChangingSubmarine && hull.Submarine != character.Submarine) { continue; } + if (hull.Rect.Height < ConvertUnits.ToDisplayUnits(character.AnimController.ColliderHeightFromFloor) * 2) { continue; } + if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; } + if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; } + if (connectedSubs != null && !connectedSubs.Contains(hull.Submarine)) { continue; } + + //sort the hulls based on distance and which sub they're in + //tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily + //(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive + //path calculations, only to discard all of them when going through the hulls in the outpost) + float hullSuitability = EstimateHullSuitability(character, hull); + if (!hulls.Any()) + { + hulls.Add(hull); + } + else + { + for (int i = 0; i < hulls.Count; i++) + { + if (hullSuitability > EstimateHullSuitability(character, hulls[i])) + { + hulls.Insert(i, hull); + break; + } + } + } + } + if (hulls.None()) + { + bestHull = null; + return HullSearchStatus.Finished; + } + hullSearchIndex = 0; + } + + static float EstimateHullSuitability(Character character, Hull hull) { float dist = Math.Abs(hull.WorldPosition.X - character.WorldPosition.X) + @@ -314,86 +387,91 @@ namespace Barotrauma return suitability; } - Hull bestHull = null; - float bestValue = 0; - bool bestIsAirlock = false; - foreach (Hull hull in Hull.HullList.OrderByDescending(h => EstimateHullSuitability(h))) + Hull potentialHull = hulls[hullSearchIndex]; + + float hullSafety = 0; + bool hullIsAirlock = false; + bool isCharacterInside = character.CurrentHull != null && character.Submarine != null; + if (isCharacterInside) { - if (hull.Submarine == null) { continue; } - // Ruins are mazes filled with water. There's no safe hulls and we don't want to use the resources on it. - if (hull.Submarine.Info.IsRuin) { continue; } - if (!allowChangingTheSubmarine && hull.Submarine != character.Submarine) { continue; } - if (hull.Rect.Height < ConvertUnits.ToDisplayUnits(character.AnimController.ColliderHeightFromFloor) * 2) { continue; } - if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; } - if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; } - float hullSafety = 0; - bool hullIsAirlock = false; - bool isCharacterInside = character.CurrentHull != null && character.Submarine != null; - if (isCharacterInside) - { - if (!character.Submarine.IsConnectedTo(hull.Submarine)) { continue; } - hullSafety = HumanAIController.GetHullSafety(hull, hull.GetConnectedHulls(true, 1), character); - float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y); - yDist = yDist > 100 ? yDist * 3 : 0; - float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist; - float distanceFactor = MathHelper.Lerp(1, 0.9f, MathUtils.InverseLerp(0, 10000, dist)); - 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) - if (hullSafety < bestValue) { continue; } + 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)); + 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) + if (hullSafety > bestHullValue) + { //avoid airlock modules if not allowed to change the sub - if (!allowChangingTheSubmarine && hull.OutpostModuleTags.Any(t => t == "airlock")) + if (allowChangingSubmarine || !potentialHull.OutpostModuleTags.Any(t => t == "airlock")) { - continue; + // Don't allow to go outside if not already outside. + var path = PathSteering.PathFinder.FindPath(character.SimPosition, potentialHull.SimPosition, character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null); + if (path.Unreachable) + { + hullSafety = 0; + HumanAIController.UnreachableHulls.Add(potentialHull); + } + else + { + // Each unsafe node reduces the hull safety value. + // Ignore the current hull, because otherwise we couldn't find a path out. + int unsafeNodes = path.Nodes.Count(n => n.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(n.CurrentHull)); + hullSafety /= 1 + unsafeNodes; + // If the target is not inside a friendly submarine, considerably reduce the hull safety. + if (!character.Submarine.IsEntityFoundOnThisSub(potentialHull, true)) + { + hullSafety /= 10; + } + } } - // Don't allow to go outside if not already outside. - var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition, character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null); - if (path.Unreachable) + else { - HumanAIController.UnreachableHulls.Add(hull); - continue; + hullSafety = 0; } - // Each unsafe node reduces the hull safety value. - // Ignore the current hull, because otherwise we couldn't find a path out. - int unsafeNodes = path.Nodes.Count(n => n.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(n.CurrentHull)); - hullSafety /= 1 + unsafeNodes; - // If the target is not inside a friendly submarine, considerably reduce the hull safety. - if (!character.Submarine.IsEntityFoundOnThisSub(hull, true)) - { - hullSafety /= 10; - } - } - else - { - // TODO: could also target gaps that get us inside? - if (hull.IsTaggedAirlock()) - { - hullSafety = 100; - hullIsAirlock = true; - } - else if(!bestIsAirlock && hull.LeadsOutside(character)) - { - hullSafety = 100; - } - // Huge preference for closer targets - float distance = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition); - float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance)); - hullSafety *= distanceFactor; - // If the target is not inside a friendly submarine, considerably reduce the hull safety. - // Intentionally exclude wrecks from this check - if (hull.Submarine.TeamID != character.TeamID && hull.Submarine.TeamID != CharacterTeamType.FriendlyNPC) - { - hullSafety /= 10; - } - } - if (hullSafety > bestValue || (!isCharacterInside && hullIsAirlock && !bestIsAirlock)) - { - bestHull = hull; - bestValue = hullSafety; - bestIsAirlock = hullIsAirlock; } } - return bestHull; + else + { + // TODO: could also target gaps that get us inside? + if (potentialHull.IsTaggedAirlock()) + { + hullSafety = 100; + hullIsAirlock = true; + } + else if(!bestHullIsAirlock && potentialHull.LeadsOutside(character)) + { + hullSafety = 100; + } + // Huge preference for closer targets + float distance = Vector2.DistanceSquared(character.WorldPosition, potentialHull.WorldPosition); + float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance)); + hullSafety *= distanceFactor; + // If the target is not inside a friendly submarine, considerably reduce the hull safety. + // Intentionally exclude wrecks from this check + if (potentialHull.Submarine.TeamID != character.TeamID && potentialHull.Submarine.TeamID != CharacterTeamType.FriendlyNPC) + { + hullSafety /= 10; + } + } + if (hullSafety > bestHullValue || (!isCharacterInside && hullIsAirlock && !bestHullIsAirlock)) + { + potentialBestHull = potentialHull; + bestHullValue = hullSafety; + bestHullIsAirlock = hullIsAirlock; + } + + bestHull = potentialBestHull; + hullSearchIndex++; + + if (hullSearchIndex >= hulls.Count) + { + hullSearchIndex = -1; + return HullSearchStatus.Finished; + } + return HullSearchStatus.Running; } public override void Reset() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 99adb5e1d..8807abfd1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -165,6 +165,7 @@ namespace Barotrauma requiredCondition = () => Leak.Submarine == character.Submarine && Leak.linkedTo.Any(e => e is Hull h && character.CurrentHull == h), + endNodeFilter = n => n.Waypoint.CurrentHull != null && Leak.linkedTo.Any(e => e is Hull h && h == n.Waypoint.CurrentHull), // The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue) SpeakCannotReachCondition = () => !CheckObjectiveSpecific() }, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 4b5aba5dc..853abb82e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -471,7 +471,8 @@ namespace Barotrauma { if (spawnItemIfNotFound) { - if (!(MapEntityPrefab.List.FirstOrDefault(me => me is ItemPrefab ip && IdentifiersOrTags.Any(id => id == ip.Identifier || ip.Tags.Contains(id))) is ItemPrefab prefab)) + 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); @@ -501,6 +502,33 @@ namespace Barotrauma } } + /// + /// Returns the "best" item to spawn when using and there's multiple suitable items. + /// Best in this context is the one that's sold at the lowest price in stores (usually the most "basic" item) + /// + /// + private ItemPrefab FindItemToSpawn() + { + ItemPrefab bestItem = null; + float lowestCost = float.MaxValue; + foreach (MapEntityPrefab prefab in MapEntityPrefab.List) + { + if (!(prefab is ItemPrefab itemPrefab)) { continue; } + if (IdentifiersOrTags.Any(id => id == prefab.Identifier || prefab.Tags.Contains(id))) + { + float cost = itemPrefab.DefaultPrice != null && itemPrefab.CanBeBought ? + itemPrefab.DefaultPrice.Price : + float.MaxValue; + if (cost < lowestCost || bestItem == null) + { + bestItem = itemPrefab; + lowestCost = cost; + } + } + } + return bestItem; + } + protected override bool CheckObjectiveSpecific() { if (IsCompleted) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index 4be832fd0..c700ff0ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using static Barotrauma.AIObjectiveFindSafety; namespace Barotrauma { @@ -186,7 +187,9 @@ namespace Barotrauma } else { - safeHull = objectiveManager.GetObjective().FindBestHull(HumanAIController.VisibleHulls); + HullSearchStatus hullSearchStatus = objectiveManager.GetObjective().FindBestHull(out Hull potentialSafeHull, HumanAIController.VisibleHulls); + if (hullSearchStatus != HullSearchStatus.Finished) { return; } + safeHull = potentialSafeHull; findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index ba7a674fb..258eb4e33 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -24,7 +24,7 @@ namespace Barotrauma public bool IsAiming => wasAiming; public bool IsAimingMelee => wasAimingMelee; - protected bool Aiming => aiming || aimingMelee; + protected bool Aiming => aiming || aimingMelee || LockFlippingUntil > Timing.TotalTime && character.IsKeyDown(InputType.Aim); public float ArmLength => upperArmLength + forearmLength; @@ -275,6 +275,8 @@ namespace Barotrauma // We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative. public bool IsAboveFloor => GetHeightFromFloor() > -0.1f; + public float LockFlippingUntil; + public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos) { useItemTimer = 0.5f; @@ -380,18 +382,10 @@ namespace Barotrauma { //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); - if (!anotherItemControlsPose) + if (!anotherItemControlsPose && TargetMovement == Vector2.Zero && inWater) { - var head = GetLimb(LimbType.Head); - if (head != null) - { - head.body.SmoothRotate(itemAngle, force: 30 * head.Mass); - } - if (TargetMovement == Vector2.Zero && inWater) - { - torso.body.AngularVelocity -= torso.body.AngularVelocity * 0.1f; - torso.body.ApplyForce(torso.body.LinearVelocity * -0.5f); - } + torso.body.AngularVelocity -= torso.body.AngularVelocity * 0.1f; + torso.body.ApplyForce(torso.body.LinearVelocity * -0.5f); } aiming = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 7953deff1..74f7ad622 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -164,8 +164,6 @@ namespace Barotrauma public float LegBendTorque => CurrentGroundedParams.LegBendTorque * RagdollParams.JointScale; public Vector2 HandMoveOffset => CurrentGroundedParams.HandMoveOffset * RagdollParams.JointScale; - public float LockFlippingUntil; - public override Vector2 AimSourceSimPos { get @@ -841,7 +839,7 @@ namespace Barotrauma rotation += 360; } float targetSpeed = TargetMovement.Length(); - if (targetSpeed > 0.1f && !character.IsRemotelyControlled && !character.IsKeyDown(InputType.Aim)) + if (targetSpeed > 0.1f && !character.IsRemotelyControlled && !Aiming) { if (Anim != Animation.UsingConstruction && !(character.SelectedConstruction?.GetComponent()?.ControlCharacterPose ?? false)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 736e50d2c..d31d943a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -153,12 +153,12 @@ namespace Barotrauma protected ActiveTeamChange currentTeamChange; const string OriginalTeamIdentifier = "original"; - public static void ThrowIfAccessingWalletsInSingleplayer() + private void ThrowIfAccessingWalletsInSingleplayer() { #if CLIENT && DEBUG if (Screen.Selected is TestScreen) { return; } #endif - if (GameMain.NetworkMember is null || GameMain.IsSingleplayer) + if ((GameMain.NetworkMember is null || GameMain.IsSingleplayer) && IsPlayer) { throw new InvalidOperationException($"Tried to access crew wallets in singleplayer. Use {nameof(CampaignMode)}.{nameof(CampaignMode.Bank)} or {nameof(CampaignMode)}.{nameof(CampaignMode.GetWallet)} instead."); } @@ -560,18 +560,35 @@ namespace Barotrauma #if CLIENT CharacterHealth.SetHealthBarVisibility(value == null); -#elif SERVER - if (value is { IsDead: true, Wallet: { Balance: var balance } grabbedWallet } && balance > 0) +#endif + bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true }; + if (IsPlayer && isServerOrSingleplayer && value is { IsDead: true, Wallet: { Balance: var balance } grabbedWallet } && balance > 0) { - if (GameMain.GameSession.Campaign is MultiPlayerCampaign mpCampaign) +#if SERVER + if (GameMain.GameSession.Campaign is MultiPlayerCampaign mpCampaign && GameMain.Server is { ServerSettings: { } settings }) { - mpCampaign.Bank.Give(balance); + switch (settings.LootedMoneyDestination) + { + case LootedMoneyDestination.Wallet when IsPlayer: + Wallet.Give(balance); + break; + default: + mpCampaign.Bank.Give(balance); + break; + + } } - grabbedWallet.Deduct(balance); GameServer.Log($"{GameServer.CharacterLogName(this)} grabbed {value.Name}'s body and received {grabbedWallet.Balance} mk.", ServerLog.MessageType.Money); - } +#elif CLIENT + if (GameMain.GameSession.Campaign is SinglePlayerCampaign spCampaign) + { + spCampaign.Bank.Give(balance); + } #endif + + grabbedWallet.Deduct(balance); + } } } @@ -1443,7 +1460,7 @@ namespace Barotrauma foreach (Item item in Inventory.AllItems) { - if (item?.Prefab.Identifier != "idcard") { continue; } + if (item?.GetComponent() == null) { continue; } foreach (string s in spawnPoint.IdCardTags) { item.AddTag(s); @@ -3954,7 +3971,10 @@ namespace Barotrauma if (actionType != ActionType.OnDamaged && actionType != ActionType.OnSevered) { // OnDamaged is called only for the limb that is hit. - AnimController.Limbs.ForEach(l => l.ApplyStatusEffects(actionType, deltaTime)); + foreach (Limb limb in AnimController.Limbs) + { + limb.ApplyStatusEffects(actionType, deltaTime); + } } //OnActive effects are handled by the afflictions themselves if (actionType != ActionType.OnActive) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 61790a3b2..7700016df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -252,12 +252,11 @@ namespace Barotrauma } /// - /// Endocrine boosters can unlock talents outside the user's talent tree. This method is used to specifically get them + /// Returns unlocked talents that aren't part of the character's talent tree (which can be unlocked e.g. with an endocrine booster) /// - public IEnumerable GetEndocrineTalents() + public IEnumerable GetUnlockedTalentsOutsideTree() { if (!TalentTree.JobTalentTrees.TryGet(Job.Prefab.Identifier, out TalentTree talentTree)) { return Enumerable.Empty(); } - return UnlockedTalents.Where(t => !talentTree.TalentIsInTree(t)); } @@ -1182,7 +1181,7 @@ namespace Barotrauma // Replace the name tag of any existing id cards or duffel bags foreach (var item in Item.ItemList) { - if (item.Prefab.Identifier != "idcard" && !item.Tags.Contains("despawncontainer")) { continue; } + if (!item.HasTag("identitycard") && !item.HasTag("despawncontainer")) { continue; } foreach (var tag in item.Tags.Split(',')) { var splitTag = tag.Split(":"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 4bc5d873b..4e277a25a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -300,6 +300,7 @@ namespace Barotrauma } public static AfflictionPrefab InternalDamage => Prefabs["internaldamage"]; + public static AfflictionPrefab BiteWounds => Prefabs["bitewounds"]; public static AfflictionPrefab ImpactDamage => Prefabs["blunttrauma"]; public static AfflictionPrefab Bleeding => Prefabs["bleeding"]; public static AfflictionPrefab Burn => Prefabs["burn"]; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 7687f0b65..5e4435b7d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -124,7 +124,18 @@ namespace Barotrauma public float PressureKillDelay { get; private set; } = 5.0f; - public float Vitality { get; private set; } + private float vitality; + public float Vitality + { + get + { + return Character.IsDead ? minVitality : vitality; + } + private set + { + vitality = value; + } + } public float HealthPercentage => MathUtils.Percentage(Vitality, MaxVitality); @@ -725,6 +736,8 @@ namespace Barotrauma AddLimbAffliction(limbHealth: null, newAffliction, allowStacking); } + partial void UpdateSkinTint(); + partial void UpdateLimbAfflictionOverlays(); public void Update(float deltaTime) @@ -788,7 +801,7 @@ namespace Barotrauma if (!Character.GodMode) { UpdateLimbAfflictionOverlays(); - UpdateSkinTint(); + UpdateSkinTint(); CalculateVitality(); if (Vitality <= MinVitality) @@ -798,23 +811,6 @@ namespace Barotrauma } } - private void UpdateSkinTint() - { - FaceTint = DefaultFaceTint; - BodyTint = Color.TransparentBlack; - - if (!(Character?.Params?.Health.ApplyAfflictionColors ?? false)) { return; } - - foreach (KeyValuePair kvp in afflictions) - { - var affliction = kvp.Key; - Color faceTint = affliction.GetFaceTint(); - if (faceTint.A > FaceTint.A) { FaceTint = faceTint; } - Color bodyTint = affliction.GetBodyTint(); - if (bodyTint.A > BodyTint.A) { BodyTint = bodyTint; } - } - } - private void UpdateDamageReductions(float deltaTime) { float healthRegen = Character.Params.Health.ConstantHealthRegeneration; @@ -905,6 +901,7 @@ namespace Barotrauma if (Unkillable || Character.GodMode) { return; } var (type, affliction) = GetCauseOfDeath(); + UpdateLimbAfflictionOverlays(); UpdateSkinTint(); Character.Kill(type, affliction); #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index 46a7da94f..c2d64b348 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -105,9 +105,9 @@ namespace Barotrauma return spawnPointTags; } - public JobPrefab GetJobPrefab(Rand.RandSync randSync = Rand.RandSync.Unsynced) + public JobPrefab GetJobPrefab(Rand.RandSync randSync = Rand.RandSync.Unsynced, Func predicate = null) { - return Job != null && Job != "any" ? JobPrefab.Get(Job) : JobPrefab.Random(randSync); + return Job != null && Job != "any" ? JobPrefab.Get(Job) : JobPrefab.Random(randSync, predicate); } public void InitializeCharacter(Character npc, ISpatialEntity positionToStayIn = null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index f9f45d936..615a33350 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -209,11 +209,8 @@ namespace Barotrauma } } - if (item.Prefab.Identifier == "idcard") - { - IdCard idCardComponent = item.GetComponent(); - idCardComponent?.Initialize(spawnPoint, character); - } + IdCard idCardComponent = item.GetComponent(); + idCardComponent?.Initialize(spawnPoint, character); foreach (WifiComponent wifiComponent in item.GetComponents()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index 1d1c112f1..f8ea728fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -293,6 +293,6 @@ namespace Barotrauma //ClothingElement = element.GetChildElement("PortraitClothing"); } - public static JobPrefab Random(Rand.RandSync sync) => Prefabs.GetRandom(p => !p.HiddenJob, sync); + public static JobPrefab Random(Rand.RandSync sync, Func predicate = null) => Prefabs.GetRandom(p => !p.HiddenJob && (predicate == null || predicate(p)), sync); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index f9c0f5562..98761b381 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -331,7 +331,7 @@ namespace Barotrauma #if CLIENT if (isSevered) { - damageOverlayStrength = 100.0f; + damageOverlayStrength = 1.0f; } #endif } @@ -597,18 +597,7 @@ namespace Barotrauma dir = Direction.Right; body = new PhysicsBody(limbParams); type = limbParams.Type; - if (limbParams.IgnoreCollisions) - { - body.CollisionCategories = Category.None; - body.CollidesWith = Category.None; - IgnoreCollisions = true; - } - else - { - //limbs don't collide with each other - body.CollisionCategories = Physics.CollisionCharacter; - body.CollidesWith = Physics.CollisionAll & ~Physics.CollisionCharacter & ~Physics.CollisionItem & ~Physics.CollisionItemBlocking; - } + IgnoreCollisions = limbParams.IgnoreCollisions; body.UserData = this; pullJoint = new FixedMouseJoint(body.FarseerBody, ConvertUnits.ToSimUnits(limbParams.PullPos * Scale)) { @@ -646,10 +635,9 @@ namespace Barotrauma } attack.DamageRange = ConvertUnits.ToDisplayUnits(attack.DamageRange); } - if (!character.VariantOf.IsEmpty) + if (character is { VariantOf: { IsEmpty: false } }) { - var attackElement = CharacterPrefab.Prefabs.TryGet(character.VariantOf, out var basePrefab) - ? basePrefab.ConfigElement.GetChildElement("attack") : null; + var attackElement = character.Params.VariantFile.Root.GetChildElement("attack"); if (attackElement != null) { attack.DamageMultiplier = attackElement.GetAttributeFloat("damagemultiplier", 1f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BallastFloraFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BallastFloraFile.cs index 1b45bc400..eb564c001 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BallastFloraFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BallastFloraFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "ballastflorabehavior"; protected override bool MatchesPlural(Identifier identifier) => identifier == "ballastflorabehaviors"; - protected override PrefabCollection prefabs => BallastFloraPrefab.Prefabs; + protected override PrefabCollection Prefabs => BallastFloraPrefab.Prefabs; protected override BallastFloraPrefab CreatePrefab(ContentXElement element) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CaveGenerationParametersFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CaveGenerationParametersFile.cs index 717057ff6..afb3296aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CaveGenerationParametersFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CaveGenerationParametersFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "cave"; protected override bool MatchesPlural(Identifier identifier) => identifier == "cavegenerationparameters"; - protected override PrefabCollection prefabs => CaveGenerationParams.CaveParams; + protected override PrefabCollection Prefabs => CaveGenerationParams.CaveParams; protected override CaveGenerationParams CreatePrefab(ContentXElement element) { return new CaveGenerationParams(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs index 1a0b569d5..e3412c1be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs @@ -77,7 +77,7 @@ namespace Barotrauma { HashSet texturePaths = new HashSet { - ragdollParams.Texture + ContentPath.FromRaw(CharacterPrefab.Prefabs[speciesName].ContentPackage, ragdollParams.Texture).Value }; foreach (RagdollParams.LimbParams limb in ragdollParams.Limbs) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CorpsesFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CorpsesFile.cs index b9eb4ddce..79e071b9e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CorpsesFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CorpsesFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "corpse"; protected override bool MatchesPlural(Identifier identifier) => identifier == "corpses"; - protected override PrefabCollection prefabs => CorpsePrefab.Prefabs; + protected override PrefabCollection Prefabs => CorpsePrefab.Prefabs; protected override CorpsePrefab CreatePrefab(ContentXElement element) { return new CorpsePrefab(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/EventManagerSettingsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/EventManagerSettingsFile.cs index 298f618c5..0e6c4c309 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/EventManagerSettingsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/EventManagerSettingsFile.cs @@ -8,7 +8,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); protected override bool MatchesPlural(Identifier identifier) => identifier == "EventManagerSettings"; - protected override PrefabCollection prefabs => EventManagerSettings.Prefabs; + protected override PrefabCollection Prefabs => EventManagerSettings.Prefabs; protected override EventManagerSettings CreatePrefab(ContentXElement element) { return new EventManagerSettings(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/FactionsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/FactionsFile.cs index bb200e5c6..fac0ae4cb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/FactionsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/FactionsFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "faction"; protected override bool MatchesPlural(Identifier identifier) => identifier == "factions"; - protected override PrefabCollection prefabs => FactionPrefab.Prefabs; + protected override PrefabCollection Prefabs => FactionPrefab.Prefabs; protected override FactionPrefab CreatePrefab(ContentXElement element) { return new FactionPrefab(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs index e59272553..cd0906887 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected abstract bool MatchesSingular(Identifier identifier); protected abstract bool MatchesPlural(Identifier identifier); - protected abstract PrefabCollection prefabs { get; } + protected abstract PrefabCollection Prefabs { get; } protected abstract T CreatePrefab(ContentXElement element); private void LoadFromXElement(ContentXElement parentElement, bool overriding) @@ -29,14 +29,14 @@ namespace Barotrauma } else if (elemName == "clear") { - prefabs.AddOverrideFile(this); + Prefabs.AddOverrideFile(this); } else if (MatchesSingular(elemName)) { T prefab = CreatePrefab(parentElement); try { - prefabs.Add(prefab, overriding); + Prefabs.Add(prefab, overriding); } catch { @@ -53,7 +53,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); + DebugConsole.ThrowError($"GenericPrefabFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); } } @@ -68,12 +68,12 @@ namespace Barotrauma public override sealed void UnloadFile() { - prefabs.RemoveByFile(this); + Prefabs.RemoveByFile(this); } public sealed override void Sort() { - prefabs.SortAll(); + Prefabs.SortAll(); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemAssemblyFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemAssemblyFile.cs index bff61b1bb..4186f99d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemAssemblyFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemAssemblyFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "itemassembly"; protected override bool MatchesPlural(Identifier identifier) => identifier == "itemassemblies"; - protected override PrefabCollection prefabs => ItemAssemblyPrefab.Prefabs; + protected override PrefabCollection Prefabs => ItemAssemblyPrefab.Prefabs; protected override ItemAssemblyPrefab CreatePrefab(ContentXElement element) { return new ItemAssemblyPrefab(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemFile.cs index 5065470c2..afb002440 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); protected override bool MatchesPlural(Identifier identifier) => identifier == "items"; - protected override PrefabCollection prefabs => ItemPrefab.Prefabs; + protected override PrefabCollection Prefabs => ItemPrefab.Prefabs; protected override ItemPrefab CreatePrefab(ContentXElement element) { return new ItemPrefab(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LevelObjectPrefabsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LevelObjectPrefabsFile.cs index 228dca7cf..2f359a366 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LevelObjectPrefabsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LevelObjectPrefabsFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); protected override bool MatchesPlural(Identifier identifier) => identifier == "levelobjects"; - protected override PrefabCollection prefabs => LevelObjectPrefab.Prefabs; + protected override PrefabCollection Prefabs => LevelObjectPrefab.Prefabs; protected override LevelObjectPrefab CreatePrefab(ContentXElement element) { return new LevelObjectPrefab(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LocationTypesFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LocationTypesFile.cs index cd3cc4c91..f9376f752 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LocationTypesFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LocationTypesFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); protected override bool MatchesPlural(Identifier identifier) => identifier == "locationtypes"; - protected override PrefabCollection prefabs => LocationType.Prefabs; + protected override PrefabCollection Prefabs => LocationType.Prefabs; protected override LocationType CreatePrefab(ContentXElement element) { return new LocationType(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/MissionsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/MissionsFile.cs index 11efb2d0c..8907524de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/MissionsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/MissionsFile.cs @@ -23,7 +23,7 @@ namespace Barotrauma /*missionTypes.Any(t => identifier == t.Name) || identifier == "OutpostDestroyMission" || identifier == "OutpostRescueMission";*/ protected override bool MatchesPlural(Identifier identifier) => identifier == "missions"; - protected override PrefabCollection prefabs => MissionPrefab.Prefabs; + protected override PrefabCollection Prefabs => MissionPrefab.Prefabs; protected override MissionPrefab CreatePrefab(ContentXElement element) { return new MissionPrefab(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCSetsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCSetsFile.cs index 85b2548d9..4433b6158 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCSetsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCSetsFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "npcset"; protected override bool MatchesPlural(Identifier identifier) => identifier == "npcsets"; - protected override PrefabCollection prefabs => NPCSet.Sets; + protected override PrefabCollection Prefabs => NPCSet.Sets; protected override NPCSet CreatePrefab(ContentXElement element) { return new NPCSet(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OrdersFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OrdersFile.cs index 5699a0410..57273ced6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OrdersFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OrdersFile.cs @@ -42,7 +42,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); + DebugConsole.ThrowError($"OrdersFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostConfigFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostConfigFile.cs index 1972243dc..1e2546859 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostConfigFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostConfigFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "OutpostConfig"; protected override bool MatchesPlural(Identifier identifier) => identifier == "OutpostGenerationParameters"; - protected override PrefabCollection prefabs => OutpostGenerationParams.OutpostParams; + protected override PrefabCollection Prefabs => OutpostGenerationParams.OutpostParams; protected override OutpostGenerationParams CreatePrefab(ContentXElement element) { return new OutpostGenerationParams(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ParticlesFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ParticlesFile.cs index de128d17c..3730156ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ParticlesFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ParticlesFile.cs @@ -14,7 +14,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); protected override bool MatchesPlural(Identifier identifier) => identifier == "prefabs" || identifier == "particles"; - protected override PrefabCollection prefabs => ParticlePrefab.Prefabs; + protected override PrefabCollection Prefabs => ParticlePrefab.Prefabs; protected override ParticlePrefab CreatePrefab(ContentXElement element) { return new ParticlePrefab(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs index d4c5b1c43..6f636c4e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs @@ -57,7 +57,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); + DebugConsole.ThrowError($"RandomEventsFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RuinConfigFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RuinConfigFile.cs index 9a8de8fd1..08ba4d436 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RuinConfigFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RuinConfigFile.cs @@ -10,7 +10,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "RuinConfig"; protected override bool MatchesPlural(Identifier identifier) => identifier == "RuinGenerationParameters"; - protected override PrefabCollection prefabs => RuinGenerationParams.RuinParams; + protected override PrefabCollection Prefabs => RuinGenerationParams.RuinParams; protected override RuinGenerationParams CreatePrefab(ContentXElement element) { return new RuinGenerationParams(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SoundsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SoundsFile.cs index 57034f4d1..b14263066 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SoundsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SoundsFile.cs @@ -11,7 +11,7 @@ namespace Barotrauma { public SoundsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } - protected override PrefabCollection prefabs => SoundPrefab.Prefabs; + protected override PrefabCollection Prefabs => SoundPrefab.Prefabs; protected override SoundPrefab CreatePrefab(ContentXElement element) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/StartItemsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/StartItemsFile.cs new file mode 100644 index 000000000..072acfe63 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/StartItemsFile.cs @@ -0,0 +1,12 @@ +namespace Barotrauma +{ + sealed class StartItemsFile : GenericPrefabFile + { + public StartItemsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "itemset"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "startitems"; + protected override PrefabCollection Prefabs => StartItemSet.Sets; + protected override StartItemSet CreatePrefab(ContentXElement element) => new StartItemSet(element, this); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/StructureFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/StructureFile.cs index b961311ab..bec8357b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/StructureFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/StructureFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); protected override bool MatchesPlural(Identifier identifier) => identifier == "prefabs" || identifier == "structures"; - protected override PrefabCollection prefabs => StructurePrefab.Prefabs; + protected override PrefabCollection Prefabs => StructurePrefab.Prefabs; protected override StructurePrefab CreatePrefab(ContentXElement element) { return new StructurePrefab(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentTreesFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentTreesFile.cs index 6ca1f9c68..cc25d8fc5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentTreesFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentTreesFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "talenttree"; protected override bool MatchesPlural(Identifier identifier) => identifier == "talenttrees"; - protected override PrefabCollection prefabs => TalentTree.JobTalentTrees; + protected override PrefabCollection Prefabs => TalentTree.JobTalentTrees; protected override TalentTree CreatePrefab(ContentXElement element) { return new TalentTree(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentsFile.cs index 1b5b05f4f..c234bd117 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentsFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "talent"; protected override bool MatchesPlural(Identifier identifier) => identifier == "talents"; - protected override PrefabCollection prefabs => TalentPrefab.TalentPrefabs; + protected override PrefabCollection Prefabs => TalentPrefab.TalentPrefabs; protected override TalentPrefab CreatePrefab(ContentXElement element) { return new TalentPrefab(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TraitorMissionsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TraitorMissionsFile.cs index 3a58364e0..a43c10379 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TraitorMissionsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TraitorMissionsFile.cs @@ -17,7 +17,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "TraitorMission"; protected override bool MatchesPlural(Identifier identifier) => identifier == "TraitorMissions"; - protected override PrefabCollection prefabs => PrefabType.Prefabs; + protected override PrefabCollection Prefabs => PrefabType.Prefabs; protected override PrefabType CreatePrefab(ContentXElement element) { return new PrefabType(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/UpgradeModulesFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/UpgradeModulesFile.cs index 61de9e96b..91e1a8d47 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/UpgradeModulesFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/UpgradeModulesFile.cs @@ -14,7 +14,7 @@ namespace Barotrauma protected override bool MatchesPlural(Identifier identifier) => identifier == "upgrademodules"; - protected override PrefabCollection prefabs => UpgradeContentPrefab.PrefabsAndCategories; + protected override PrefabCollection Prefabs => UpgradeContentPrefab.PrefabsAndCategories; protected override UpgradeContentPrefab CreatePrefab(ContentXElement element) { Identifier elemName = element.NameAsIdentifier(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/WreckAIConfigFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/WreckAIConfigFile.cs index 54a445ff0..be1c2ef5c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/WreckAIConfigFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/WreckAIConfigFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma protected override bool MatchesSingular(Identifier identifier) => identifier == "wreckaiconfig"; protected override bool MatchesPlural(Identifier identifier) => identifier == "wreckaiconfigs"; - protected override PrefabCollection prefabs => WreckAIConfig.Prefabs; + protected override PrefabCollection Prefabs => WreckAIConfig.Prefabs; protected override WreckAIConfig CreatePrefab(ContentXElement element) { return new WreckAIConfig(element, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index eba749c54..da7f6be5a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -14,8 +14,7 @@ namespace Barotrauma { public abstract class ContentPackage { - #warning TODO: make this independent of the current version - public static readonly Version MinimumHashCompatibleVersion = GameMain.Version; + public static readonly Version MinimumHashCompatibleVersion = new Version(0, 17, 16, 0); public const string LocalModsDir = "LocalMods"; public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine( diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs index d105e09cc..b7388bb2b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs @@ -49,7 +49,10 @@ namespace Barotrauma .Replace(string.Format(OtherModDirFmt, ContentPackage.SteamWorkshopId.ToString(CultureInfo.InvariantCulture)), modPath, StringComparison.OrdinalIgnoreCase); } } - var allPackages = ContentPackageManager.EnabledPackages.All; + var allPackages = ContentPackageManager.AllPackages; +#if CLIENT + if (GameMain.ModDownloadScreen?.DownloadedPackages != null) { allPackages = allPackages.Concat(GameMain.ModDownloadScreen.DownloadedPackages); } +#endif foreach (Identifier otherModName in otherMods) { if (!UInt64.TryParse(otherModName.Value, out UInt64 workshopId)) { workshopId = 0; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs index 5711e7769..f5dba0b3d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs @@ -51,7 +51,7 @@ namespace Barotrauma => Element.Descendants().Select(e => new ContentXElement(ContentPackage, e)); public IEnumerable GetChildElements(string name) - => Elements().Where(e => string.Equals(name, e.Name.LocalName, StringComparison.CurrentCultureIgnoreCase)); + => Elements().Where(e => string.Equals(name, e.Name.LocalName, StringComparison.InvariantCultureIgnoreCase)); public XAttribute? GetAttribute(string name) => Element.GetAttribute(name); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/MissingContentPackageException.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/MissingContentPackageException.cs index f6fb91198..2c6d0df5e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/MissingContentPackageException.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/MissingContentPackageException.cs @@ -11,7 +11,7 @@ namespace Barotrauma { Message = $"\"{whoAsked?.Name ?? "[NULL]"}\" depends on a package " + $"with name or ID \"{missingPackage ?? "[NULL]"}\" " + - $"that is not currently enabled."; + $"that is not currently installed."; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index afa6c9758..3369bb456 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -780,7 +780,7 @@ namespace Barotrauma return; } GameMain.GameSession.EventManager.ActiveEvents.Add(newEvent); - newEvent.Init(true); + newEvent.Init(); NewMessage($"Initialized event {eventPrefab.Identifier}", Color.Aqua); return; } @@ -1829,6 +1829,17 @@ namespace Barotrauma })); #endif + commands.Add(new Command("startitems|startitemset", "start item set identifier", (string[] args) => + { + if (args.Length == 0) + { + ThrowError($"No start item set identifier defined!"); + return; + } + AutoItemPlacer.StartItemSet = args[0].ToIdentifier(); + NewMessage($"Start item set changed to \"{AutoItemPlacer.StartItemSet}\""); + }, isCheat: false)); + //"dummy commands" that only exist so that the server can give clients permissions to use them //TODO: alphabetical order? commands.Add(new Command("control", "control [character name]: Start controlling the specified character (client-only).", null, () => diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs index 3c8e23fd4..2275bcd06 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs @@ -53,8 +53,9 @@ namespace Barotrauma } } - public override void Init(bool affectSubImmediately) + public override void Init(EventSet parentSet) { + base.Init(parentSet); spawnPos = Level.Loaded.GetRandomItemPos( (Rand.Value(Rand.RandSync.ServerAndClient) < 0.5f) ? Level.PositionType.MainPath | Level.PositionType.SidePath : @@ -111,7 +112,7 @@ namespace Barotrauma case 1: if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) return; - Finished(); + Finish(); state = 2; break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs index be1857c50..192664d17 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs @@ -5,13 +5,16 @@ using System.Collections.Generic; namespace Barotrauma { class Event - { + { + public event Action Finished; protected bool isFinished; protected readonly EventPrefab prefab; public EventPrefab Prefab => prefab; + public EventSet ParentSet { get; private set; } + public Func SpawnPosFilter; public bool IsFinished @@ -42,23 +45,20 @@ namespace Barotrauma yield break; } - public virtual void Init(bool affectSubImmediately) + public virtual void Init(EventSet parentSet = null) { + ParentSet = parentSet; } public virtual void Update(float deltaTime) { } - public virtual void Finished() + public virtual void Finish() { isFinished = true; - } - - public virtual bool CanAffectSubImmediately(Level level) - { - return true; - } + Finished?.Invoke(); + } public virtual bool LevelMeetsRequirements() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 8bbb511bf..c295eaa49 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -117,6 +117,8 @@ namespace Barotrauma public bool Enabled = true; + private MTRandom rand; + public void StartRound(Level level) { this.level = level; @@ -147,7 +149,7 @@ namespace Barotrauma seed ^= ToolBox.IdentifierToInt(previousEvent.Identifier); } } - MTRandom rand = new MTRandom(seed); + rand = new MTRandom(seed); EventSet initialEventSet = SelectRandomEvents(EventSet.Prefabs.ToList(), requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand); EventSet additiveSet = null; @@ -159,12 +161,12 @@ namespace Barotrauma if (initialEventSet != null) { pendingEventSets.Add(initialEventSet); - CreateEvents(initialEventSet, rand); + CreateEvents(initialEventSet); } if (additiveSet != null) { pendingEventSets.Add(additiveSet); - CreateEvents(additiveSet, rand); + CreateEvents(additiveSet); } if (level?.LevelData?.Type == LevelData.LevelType.Outpost) @@ -183,7 +185,7 @@ namespace Barotrauma if (unlockPathEventPrefab != null) { var newEvent = unlockPathEventPrefab.CreateInstance(); - newEvent.Init(true); + newEvent.Init(); ActiveEvents.Add(newEvent); } else @@ -362,8 +364,9 @@ namespace Barotrauma return retVal; } - private void CreateEvents(EventSet eventSet, Random rand) + private void CreateEvents(EventSet eventSet) { + selectedEvents.Remove(eventSet); if (level == null) { return; } if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; } DebugConsole.NewMessage($"Loading event set {eventSet.Identifier}", Color.LightBlue, debugOnly: true); @@ -421,7 +424,7 @@ namespace Barotrauma var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } - newEvent.Init(true); + newEvent.Init(eventSet); if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; } DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true); if (!selectedEvents.ContainsKey(eventSet)) @@ -438,7 +441,7 @@ namespace Barotrauma var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: rand); if (newEventSet != null) { - CreateEvents(newEventSet, rand); + CreateEvents(newEventSet); } } } @@ -451,7 +454,7 @@ namespace Barotrauma var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, rand); var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } - newEvent.Init(true); + newEvent.Init(eventSet); DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true); if (!selectedEvents.ContainsKey(eventSet)) { @@ -465,7 +468,7 @@ namespace Barotrauma { if (!IsValidForLevel(childEventSet, level)) { continue; } if (location != null && !IsValidForLocation(childEventSet, location)) { continue; } - CreateEvents(childEventSet, rand); + CreateEvents(childEventSet); } } } @@ -666,6 +669,14 @@ namespace Barotrauma { eventCoolDown = settings.EventCooldown; } + if (eventSet.ResetTime > 0) + { + ev.Finished += () => + { + pendingEventSets.Add(eventSet); + CreateEvents(eventSet); + }; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index 150aee8b6..ebd615008 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -58,7 +58,7 @@ namespace Barotrauma } #endif - public static List GetAllEventPrefabs() + public static List GetAllEventPrefabs() { List eventPrefabs = EventPrefab.Prefabs.ToList(); foreach (var eventSet in Prefabs) @@ -118,6 +118,8 @@ namespace Barotrauma public readonly float DefaultCommonness; public readonly ImmutableDictionary OverrideCommonness; + public readonly float ResetTime; + public readonly struct SubEventPrefab { public SubEventPrefab(Either prefabOrIdentifiers, float? commonness, float? probability) @@ -244,6 +246,7 @@ namespace Barotrauma OncePerOutpost = element.GetAttributeBool("onceperoutpost", false); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost || (parentSet?.IsCampaignSet ?? false)); + ResetTime = element.GetAttributeFloat("resettime", 0); DefaultCommonness = 1.0f; foreach (var subElement in element.Elements()) @@ -454,7 +457,6 @@ namespace Barotrauma { childSet.Dispose(); } - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs index f716173e0..47c73a3eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs @@ -39,13 +39,9 @@ namespace Barotrauma targetItemIdentifiers = prefab.ConfigElement.GetAttributeIdentifierArray("itemidentifiers", Array.Empty()); } - public override bool CanAffectSubImmediately(Level level) - { - return Item.ItemList.Count(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier)) >= maxItemAmount; - } - - public override void Init(bool affectSubImmediately) + public override void Init(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++) @@ -60,7 +56,7 @@ namespace Barotrauma if (isFinished) return; if (targetItems.Count == 0 || timer >= duration) { - Finished(); + Finish(); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index dc062b81e..4975202c3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -323,7 +323,7 @@ namespace Barotrauma { var newEvent = eventPrefab.CreateInstance(); GameMain.GameSession.EventManager.ActiveEvents.Add(newEvent); - newEvent.Init(true); + newEvent.Init(); } } @@ -382,7 +382,8 @@ namespace Barotrauma #if SERVER totalReward = DistributeRewardsToCrew(GameSession.GetSessionCrewCharacters(CharacterType.Player), totalReward); #endif - if (totalReward > 0) + bool isSingleplayerOrServer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true }; + if (isSingleplayerOrServer && totalReward > 0) { campaign.Bank.Give(totalReward); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 796ae18b0..1be2ee4d3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -146,8 +146,15 @@ namespace Barotrauma tags = element.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); - Name = TextManager.Get($"MissionName.{TextIdentifier}").Fallback(element.GetAttributeString("name", "")); - Description = TextManager.Get($"MissionDescription.{TextIdentifier}").Fallback(element.GetAttributeString("description", "")); + Name = + TextManager.Get($"MissionName.{TextIdentifier}") + .Fallback(TextManager.Get(element.GetAttributeString("name", ""))) + .Fallback(element.GetAttributeString("name", "")); + Description = + TextManager.Get($"MissionDescription.{TextIdentifier}") + .Fallback(TextManager.Get(element.GetAttributeString("description", ""))) + .Fallback(element.GetAttributeString("description", "")); + Reward = element.GetAttributeInt("reward", 1); AllowRetry = element.GetAttributeBool("allowretry", false); IsSideObjective = element.GetAttributeBool("sideobjective", false); @@ -160,10 +167,15 @@ namespace Barotrauma Difficulty = Math.Clamp(difficulty, MinDifficulty, MaxDifficulty); } - SuccessMessage = TextManager.Get($"MissionSuccess.{TextIdentifier}").Fallback(element.GetAttributeString("successmessage", "Mission completed successfully")); - FailureMessage = TextManager.Get($"MissionFailure.{TextIdentifier}").Fallback( - TextManager.Get("missionfailed")).Fallback( - GameSettings.CurrentConfig.Language == TextManager.DefaultLanguage ? element.GetAttributeString("failuremessage", "") : ""); + SuccessMessage = + TextManager.Get($"MissionSuccess.{TextIdentifier}") + .Fallback(TextManager.Get(element.GetAttributeString("successmessage", ""))) + .Fallback(element.GetAttributeString("successmessage", "Mission completed successfully")); + FailureMessage = + TextManager.Get($"MissionFailure.{TextIdentifier}") + .Fallback(TextManager.Get(element.GetAttributeString("missionfailed", ""))) + .Fallback(TextManager.Get("missionfailed")) + .Fallback(GameSettings.CurrentConfig.Language == TextManager.DefaultLanguage ? element.GetAttributeString("failuremessage", "") : ""); string sonarLabelTag = element.GetAttributeString("sonarlabel", ""); @@ -208,8 +220,14 @@ namespace Barotrauma headers.Add(string.Empty); messages.Add(string.Empty); } - headers[messageIndex] = TextManager.Get($"MissionHeader{messageIndex}.{TextIdentifier}").Fallback(subElement.GetAttributeString("header", "")); - messages[messageIndex] = TextManager.Get($"MissionMessage{messageIndex}.{TextIdentifier}").Fallback(subElement.GetAttributeString("text", "")); + headers[messageIndex] = + TextManager.Get($"MissionHeader{messageIndex}.{TextIdentifier}") + .Fallback(TextManager.Get(subElement.GetAttributeString("header", ""))) + .Fallback(subElement.GetAttributeString("header", "")); + messages[messageIndex] = + TextManager.Get($"MissionMessage{messageIndex}.{TextIdentifier}") + .Fallback(TextManager.Get(subElement.GetAttributeString("text", ""))) + .Fallback(subElement.GetAttributeString("text", "")); messageIndex++; break; case "locationtype": diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index 6e0b0abc9..20d26fbf5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -270,7 +270,7 @@ namespace Barotrauma foreach (Item item in spawnedCharacter.Inventory.AllItems) { - if (item?.Prefab.Identifier == "idcard") + if (item?.GetComponent() != null) { item.AddTag("id_pirate"); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 4093d6923..66bd8857f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -16,6 +16,8 @@ namespace Barotrauma private readonly float scatter; private readonly float offset; private readonly float delayBetweenSpawns; + private float resetTime; + private float resetTimer; private Vector2? spawnPos; @@ -24,7 +26,7 @@ namespace Barotrauma public readonly Level.PositionType SpawnPosType; private readonly string spawnPointTag; - private bool spawnPending; + private bool spawnPending, spawnReady; public readonly int MaxAmountPerLevel = int.MaxValue; @@ -96,6 +98,7 @@ namespace Barotrauma offset = prefab.ConfigElement.GetAttributeFloat("offset", 0); scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 500), 0, 3000); delayBetweenSpawns = prefab.ConfigElement.GetAttributeFloat("delaybetweenspawns", 0.1f); + resetTime = prefab.ConfigElement.GetAttributeFloat("resettime", 0); if (GameMain.NetworkMember != null) { @@ -131,14 +134,14 @@ namespace Barotrauma } } - public override bool CanAffectSubImmediately(Level level) - { - float maxRange = Sonar.DefaultSonarRange * 0.8f; - return GetAvailableSpawnPositions().Any(p => Vector2.DistanceSquared(p.Position.ToVector2(), GetReferenceSub().WorldPosition) < maxRange * maxRange); - } - - public override void Init(bool affectSubImmediately) + public override void Init(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. + resetTime = parentSet.ResetTime; + } if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("Initialized MonsterEvent (" + SpeciesName + ")", Color.White); @@ -199,7 +202,7 @@ namespace Barotrauma { //no suitable position found, disable the event spawnPos = null; - Finished(); + Finish(); return; } Submarine refSub = GetReferenceSub(); @@ -267,22 +270,17 @@ namespace Barotrauma if (!isRuinOrWreck) { float minDistance = 20000; - var refSub = GetReferenceSub(); - availablePositions.RemoveAll(p => Vector2.DistanceSquared(refSub.WorldPosition, p.Position.ToVector2()) < minDistance * minDistance); - if (Submarine.MainSubs.Length > 1) + for (int i = 0; i < Submarine.MainSubs.Length; i++) { - for (int i = 1; i < Submarine.MainSubs.Length; i++) - { - if (Submarine.MainSubs[i] == null) { continue; } - availablePositions.RemoveAll(p => Vector2.DistanceSquared(Submarine.MainSubs[i].WorldPosition, p.Position.ToVector2()) < minDistance * minDistance); - } + if (Submarine.MainSubs[i] == null) { continue; } + availablePositions.RemoveAll(p => Vector2.DistanceSquared(Submarine.MainSubs[i].WorldPosition, p.Position.ToVector2()) < minDistance * minDistance); } } if (availablePositions.None()) { //no suitable position found, disable the event spawnPos = null; - Finished(); + Finish(); return; } chosenPosition = availablePositions.GetRandomUnsynced(); @@ -306,7 +304,7 @@ namespace Barotrauma { //no suitable position found, disable the event spawnPos = null; - Finished(); + Finish(); return; } } @@ -344,7 +342,7 @@ namespace Barotrauma { //no suitable position found, disable the event spawnPos = null; - Finished(); + Finish(); return; } } @@ -352,20 +350,42 @@ namespace Barotrauma } } - private float GetMinDistanceToSub(Submarine submarine) + private float GetMinDistanceToSub(Submarine submarine) { - return Math.Max(Math.Max(submarine.Borders.Width, submarine.Borders.Height), Sonar.DefaultSonarRange * 0.9f); + float minDist = Math.Max(Math.Max(submarine.Borders.Width, submarine.Borders.Height), Sonar.DefaultSonarRange * 0.9f); + if (SpawnPosType.HasFlag(Level.PositionType.Abyss)) + { + minDist *= 2; + } + return minDist; } public override void Update(float deltaTime) { if (disallowed) { - Finished(); + Finish(); return; } - if (isFinished) { return; } + if (resetTimer > 0) + { + resetTimer -= deltaTime; + if (resetTimer <= 0) + { + if (ParentSet?.ResetTime > 0) + { + // If parent has reset time defined, the set is recreated. Otherwise we'll just reset this event. + Finish(); + } + else + { + spawnReady = false; + spawnPos = null; + } + } + return; + } if (spawnPos == null) { @@ -373,7 +393,11 @@ namespace Barotrauma { if (Character.CharacterList.Count(c => c.SpeciesName == SpeciesName) >= MaxAmountPerLevel) { - disallowed = true; + // If the event is set to reset, let's just wait until the old corpse is removed (after being disabled). + if (resetTime == 0) + { + disallowed = true; + } return; } } @@ -384,9 +408,14 @@ namespace Barotrauma spawnPending = true; } - bool spawnReady = false; if (spawnPending) { + System.Diagnostics.Debug.Assert(spawnPos.HasValue); + if (spawnPos == null) + { + Finish(); + return; + } //wait until there are no submarines at the spawnpos if (SpawnPosType.HasFlag(Level.PositionType.MainPath) || SpawnPosType.HasFlag(Level.PositionType.SidePath) || SpawnPosType.HasFlag(Level.PositionType.Abyss)) { @@ -554,28 +583,24 @@ namespace Barotrauma } } - if (!spawnReady) { return; } - - Entity targetEntity = Submarine.FindClosest(GameMain.GameScreen.Cam.WorldViewCenter); -#if CLIENT - if (Character.Controlled != null) { targetEntity = Character.Controlled; } -#endif - - bool monstersDead = true; - foreach (Character monster in monsters) + if (spawnReady) { - if (!monster.IsDead) + if (monsters.None()) { - monstersDead = false; - - if (targetEntity != null && Vector2.DistanceSquared(monster.WorldPosition, targetEntity.WorldPosition) < 5000.0f * 5000.0f) + Finish(); + } + else if (monsters.All(m => m.IsDead)) + { + if (resetTime > 0) { - break; + resetTimer = resetTime; + } + else + { + Finish(); } } } - - if (monstersDead) { Finished(); } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index c41676ff9..49631f810 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -173,14 +173,14 @@ namespace Barotrauma if (!Actions.Any()) { - Finished(); + Finish(); return; } var currentAction = Actions[CurrentActionIndex]; if (!currentAction.CanBeFinished()) { - Finished(); + Finish(); return; } @@ -207,7 +207,7 @@ namespace Barotrauma if (CurrentActionIndex >= Actions.Count || CurrentActionIndex < 0) { - Finished(); + Finish(); } } else @@ -232,9 +232,9 @@ namespace Barotrauma return false; } - public override void Finished() + public override void Finish() { - base.Finished(); + base.Finish(); GameAnalyticsManager.AddDesignEvent($"ScriptedEvent:{prefab.Identifier}:Finished:{CurrentActionIndex}"); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 7311d6c05..733238123 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -3,36 +3,32 @@ using System; using System.Collections.Generic; using System.Linq; using Barotrauma.Extensions; +using Microsoft.Xna.Framework; namespace Barotrauma { - #warning TODO: This class needs some changes: - // - We shouldn't be iterating over MapEntityPrefab.List. It has no guarantee of any sort of order and becomes entirely unpredictable once you start adding mods. - // - Note: iterating over ItemPrefab.Prefabs would also be incorrect. Sorting by UintIdentifier is necessary for determinism. - // - SpawnItems and SpawnItem are named incorrectly. static class AutoItemPlacer { public static bool OutputDebugInfo = false; - /// - /// If we are spawning in an area where difficulty should not be a factor, assume difficulty is at the exact "middle" - /// - public const float DefaultDifficultyModifier = 0f; - - public static void PlaceIfNeeded() + public static void SpawnItems() { if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; } - for (int i = 0; i < Submarine.MainSubs.Length; i++) + bool skipMainSubs = GameMain.GameSession.GameMode is CampaignMode { IsFirstRound: false }; + if (!skipMainSubs) { - if (Submarine.MainSubs[i] == null || Submarine.MainSubs[i].Info.InitialSuppliesSpawned) { continue; } - List subs = new List() { Submarine.MainSubs[i] }; - subs.AddRange(Submarine.MainSubs[i].DockedTo.Where(d => !d.Info.IsOutpost)); - Place(subs); - subs.ForEach(s => s.Info.InitialSuppliesSpawned = true); + for (int i = 0; i < Submarine.MainSubs.Length; i++) + { + var sub = Submarine.MainSubs[i]; + if (sub == null || sub.Info.InitialSuppliesSpawned) { continue; } + SpawnStartItems(sub); + var subs = sub.GetConnectedSubs().Where(s => s.TeamID == sub.TeamID); + CreateAndPlace(subs); + subs.ForEach(s => s.Info.InitialSuppliesSpawned = true); + } } - float difficultyModifier = GetLevelDifficultyModifier(); foreach (var sub in Submarine.Loaded) { if (sub.Info.Type == SubmarineType.Player || @@ -42,33 +38,93 @@ namespace Barotrauma { continue; } - Place(sub.ToEnumerable(), difficultyModifier: difficultyModifier); + if (sub.Info.InitialSuppliesSpawned) { continue; } + CreateAndPlace(sub.ToEnumerable()); + sub.Info.InitialSuppliesSpawned = true; } if (Level.Loaded?.StartOutpost != null && Level.Loaded.Type == LevelData.LevelType.Outpost) { - Rand.SetSyncedSeed(ToolBox.StringToInt(Level.Loaded.StartOutpost.Info.Name)); - Place(Level.Loaded.StartOutpost.ToEnumerable()); + var sub = Level.Loaded.StartOutpost; + if (!sub.Info.InitialSuppliesSpawned) + { + Rand.SetSyncedSeed(ToolBox.StringToInt(sub.Info.Name)); + CreateAndPlace(sub.ToEnumerable()); + sub.Info.InitialSuppliesSpawned = true; + } } } - private const float MaxDifficultyModifier = 0.2f; - - /// - /// Spawn probability of loot is modified by difficulty, -20% less loot at 0% difficulty and +20% loot at 100% difficulty. - /// - private static float GetLevelDifficultyModifier() - { - return Math.Clamp(Level.Loaded?.Difficulty is float difficulty ? (difficulty / 100f) * (MaxDifficultyModifier * 2) - MaxDifficultyModifier : DefaultDifficultyModifier, -MaxDifficultyModifier, MaxDifficultyModifier); - } - public static void RegenerateLoot(Submarine sub, ItemContainer regeneratedContainer) { - // Level difficulty currently doesn't affect regenerated loot for the sake of simplicity - Place(sub.ToEnumerable(), regeneratedContainer: regeneratedContainer); + CreateAndPlace(sub.ToEnumerable(), regeneratedContainer: regeneratedContainer); } - private static void Place(IEnumerable subs, ItemContainer regeneratedContainer = null, float difficultyModifier = DefaultDifficultyModifier) + public static Identifier StartItemSet = new Identifier("normal"); + private static void SpawnStartItems(Submarine sub) + { + if (!Barotrauma.StartItemSet.Sets.TryGet(StartItemSet, out StartItemSet itemSet)) + { + DebugConsole.AddWarning($"Couldn't find a start item set matching the identifier \"{StartItemSet}\"!"); + return; + } + WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, sub); + ISpatialEntity initialSpawnPos; + if (wp?.CurrentHull == null) + { + var spawnHull = Hull.HullList.Where(h => h.Submarine == sub && !h.IsWetRoom).GetRandomUnsynced(); + if (spawnHull == null) + { + DebugConsole.AddWarning($"Failed to spawn start items in the sub. No cargo waypoint or dry hulls found to spawn the items in."); + return; + } + initialSpawnPos = spawnHull; + + } + else + { + initialSpawnPos = wp; + } + var newItems = new List(); + foreach (var startItem in itemSet.Items) + { + if (!ItemPrefab.Prefabs.TryGet(startItem.Item, out ItemPrefab itemPrefab)) + { + DebugConsole.AddWarning($"Cannot find a start item with with the identifier \"{startItem.Item}\""); + continue; + } + for (int i = 0; i < startItem.Amount; i++) + { + var item = new Item(itemPrefab, initialSpawnPos.Position, sub, callOnItemLoaded: false); + // Is this necessary? + foreach (WifiComponent wifiComponent in item.GetComponents()) + { + wifiComponent.TeamID = sub.TeamID; + } + newItems.Add(item); + } + } + var cargoContainers = new List(); + foreach (var item in newItems) + { +#if SERVER + Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); +#endif + foreach (ItemComponent ic in item.Components) + { + ic.OnItemLoaded(); + } + var container = sub.FindContainerFor(item, onlyPrimary: true); + if (container == null) + { + var cargoContainer = CargoManager.GetOrCreateCargoContainerFor(item.Prefab, initialSpawnPos, ref cargoContainers); + container = cargoContainer?.Item; + } + container?.OwnInventory.TryPutItem(item, user: null); + } + } + + private static void CreateAndPlace(IEnumerable subs, ItemContainer regeneratedContainer = null) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { @@ -76,7 +132,7 @@ namespace Barotrauma return; } - List spawnedItems = new List(100); + List itemsToSpawn = new List(100); int itemCountApprox = MapEntityPrefab.List.Count() / 3; var containers = new List(70 + 30 * subs.Count()); @@ -100,11 +156,11 @@ namespace Barotrauma containers.Shuffle(Rand.RandSync.ServerAndClient); } - foreach (ItemPrefab ip in ItemPrefab.Prefabs) + var itemPrefabs = ItemPrefab.Prefabs.OrderBy(p => p.UintIdentifier); + foreach (ItemPrefab ip in itemPrefabs) { if (!ip.PreferredContainers.Any()) { continue; } - if (ip.ConfigElement.Elements().Any(e => string.Equals(e.Name.ToString(), typeof(ItemContainer).Name.ToString(), StringComparison.OrdinalIgnoreCase)) && - ItemPrefab.Prefabs.Any(ip2 => CanSpawnIn(ip2, ip))) + if (ip.ConfigElement.Elements().Any(e => string.Equals(e.Name.ToString(), typeof(ItemContainer).Name.ToString(), StringComparison.OrdinalIgnoreCase)) && itemPrefabs.Any(ip2 => CanSpawnIn(ip2, ip))) { prefabsItemsCanSpawnIn.Add(ip); } @@ -141,9 +197,9 @@ namespace Barotrauma { var subNames = subs.Select(s => s.Info.Name).ToList(); DebugConsole.NewMessage($"Automatically placed items in { string.Join(", ", subNames) }:"); - foreach (string itemName in spawnedItems.Select(it => it.Name).Distinct()) + foreach (string itemName in itemsToSpawn.Select(it => it.Name).Distinct()) { - DebugConsole.NewMessage(" - " + itemName + " x" + spawnedItems.Count(it => it.Name == itemName)); + DebugConsole.NewMessage(" - " + itemName + " x" + itemsToSpawn.Count(it => it.Name == itemName)); } } @@ -153,24 +209,28 @@ namespace Barotrauma { foreach (Location.TakenItem takenItem in GameMain.GameSession.StartLocation.TakenItems) { - var matchingItem = spawnedItems.Find(it => takenItem.Matches(it)); + var matchingItem = itemsToSpawn.Find(it => takenItem.Matches(it)); if (matchingItem == null) { continue; } - var containedItems = spawnedItems.FindAll(it => it.ParentInventory?.Owner == matchingItem); + if (OutputDebugInfo) + { + DebugConsole.NewMessage($"Removing the stolen item: {matchingItem.Prefab.Identifier} ({matchingItem.ID})"); + } + var containedItems = itemsToSpawn.FindAll(it => it.ParentInventory?.Owner == matchingItem); matchingItem.Remove(); - spawnedItems.Remove(matchingItem); + itemsToSpawn.Remove(matchingItem); foreach (Item containedItem in containedItems) { containedItem.Remove(); - spawnedItems.Remove(containedItem); + itemsToSpawn.Remove(containedItem); } } } - foreach (Item spawnedItem in spawnedItems) + foreach (Item item in itemsToSpawn) { #if SERVER - Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(spawnedItem)); + Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); #endif - foreach (ItemComponent ic in spawnedItem.Components) + foreach (ItemComponent ic in item.Components) { ic.OnItemLoaded(); } @@ -186,9 +246,12 @@ namespace Barotrauma return false; } bool success = false; + bool isCampaign = GameMain.GameSession?.GameMode is CampaignMode; foreach (PreferredContainer preferredContainer in itemPrefab.PreferredContainers) { - if (preferredContainer.SpawnProbability <= 0.0f || preferredContainer.MaxAmount <= 0) { continue; } + if (preferredContainer.CampaignOnly && !isCampaign) { continue; } + if (preferredContainer.NotCampaign && isCampaign) { continue; } + if (preferredContainer.SpawnProbability <= 0.0f || preferredContainer.MaxAmount <= 0 && preferredContainer.Amount <= 0) { continue; } validContainers = GetValidContainers(preferredContainer, containers, validContainers, primary: true); if (validContainers.None()) { @@ -196,10 +259,10 @@ namespace Barotrauma } foreach (var validContainer in validContainers) { - var newItems = SpawnItem(itemPrefab, containers, validContainer, difficultyModifier); + var newItems = CreateItems(itemPrefab, containers, validContainer); if (newItems.Any()) { - spawnedItems.AddRange(newItems); + itemsToSpawn.AddRange(newItems); success = true; } } @@ -238,16 +301,20 @@ namespace Barotrauma (3, 0.0f), }; - private static List SpawnItem(ItemPrefab itemPrefab, List containers, KeyValuePair validContainer, float difficultyModifier) + private static List CreateItems(ItemPrefab itemPrefab, List containers, KeyValuePair validContainer) { - List spawnedItems = new List(); - if (Rand.Value(Rand.RandSync.ServerAndClient) > validContainer.Value.SpawnProbability * (1f + difficultyModifier)) { return spawnedItems; } + List newItems = new List(); + if (Rand.Value(Rand.RandSync.ServerAndClient) > validContainer.Value.SpawnProbability) { return newItems; } // Don't add dangerously reactive materials in thalamus wrecks if (validContainer.Key.Item.Submarine.WreckAI != null && itemPrefab.Tags.Contains("explodesinwater")) { - return spawnedItems; + return newItems; + } + int amount = validContainer.Value.Amount; + if (amount == 0) + { + amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1, Rand.RandSync.ServerAndClient); } - int amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1, Rand.RandSync.ServerAndClient); for (int i = 0; i < amount; i++) { if (validContainer.Key.Inventory.IsFull(takeStacksIntoAccount: true)) @@ -255,14 +322,12 @@ namespace Barotrauma containers.Remove(validContainer.Key); break; } - var existingItem = validContainer.Key.Inventory.AllItems.FirstOrDefault(it => it.Prefab == itemPrefab); int quality = existingItem?.Quality ?? ToolBox.SelectWeightedRandom( qualityCommonnesses.Select(q => q.quality).ToList(), - qualityCommonnesses.Select(q => q.commonness).ToList(), - Rand.RandSync.ServerAndClient); + qualityCommonnesses.Select(q => q.commonness).ToList(), Rand.RandSync.ServerAndClient); if (!validContainer.Key.Inventory.CanBePut(itemPrefab, quality: quality)) { break; } var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine, callOnItemLoaded: false) { @@ -277,11 +342,11 @@ namespace Barotrauma { wifiComponent.TeamID = validContainer.Key.Item.Submarine.TeamID; } - spawnedItems.Add(item); + newItems.Add(item); validContainer.Key.Inventory.TryPutItem(item, null, createNetworkEvent: false); containers.AddRange(item.GetComponents()); } - return spawnedItems; + return newItems; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index 2492beea5..b14a19dde 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -22,14 +22,14 @@ namespace Barotrauma public int Quantity { get; set; } public bool? IsStoreComponentEnabled { get; set; } - public readonly int BuyerCharacterInfoId; + public readonly int BuyerCharacterInfoIdentifier; public PurchasedItem(ItemPrefab itemPrefab, int quantity, int buyerCharacterInfoId) { ItemPrefabIdentifier = itemPrefab.Identifier; Quantity = quantity; IsStoreComponentEnabled = null; - BuyerCharacterInfoId = buyerCharacterInfoId; + BuyerCharacterInfoIdentifier = buyerCharacterInfoId; } #if CLIENT @@ -44,7 +44,7 @@ namespace Barotrauma ItemPrefabIdentifier = itemPrefabId; Quantity = quantity; IsStoreComponentEnabled = null; - BuyerCharacterInfoId = buyer?.Character?.Info?.ID ?? Character.Controlled?.Info?.ID ?? 0; + BuyerCharacterInfoIdentifier = buyer?.Character?.Info?.GetIdentifier() ?? Character.Controlled?.Info?.GetIdentifier() ?? 0; } public override string ToString() @@ -284,11 +284,10 @@ namespace Barotrauma 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 @@ -317,7 +316,10 @@ namespace Barotrauma // Exchange money int itemValue = item.Quantity * buyValues[item.ItemPrefab]; campaign.TryPurchase(client, itemValue); - GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value); + if (GameMain.IsSingleplayer) + { + GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value); + } store.Balance += itemValue; if (removeFromCrate) { @@ -368,12 +370,13 @@ namespace Barotrauma public void CreatePurchasedItems() { + purchasedIDCards.Clear(); var items = new List(); foreach (var storeSpecificItems in PurchasedItems) { items.AddRange(storeSpecificItems.Value); } - CreateItems(items, Submarine.MainSub); + CreateItems(items, Submarine.MainSub, this); PurchasedItems.Clear(); OnPurchasedItemsChanged?.Invoke(); } @@ -407,7 +410,7 @@ namespace Barotrauma if (!item.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { return false; } if (!item.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { return false; } if (!ItemAndAllContainersInteractable(item)) { return false; } - if (item.GetRootContainer() is Item rootContainer && rootContainer.HasTag("donttakeitems")) { return false; } + if (item.GetRootContainer() is Item rootContainer && rootContainer.HasTag("dontsellitems")) { return false; } return true; }).Distinct(); @@ -428,7 +431,7 @@ namespace Barotrauma if (!item.Prefab.CanBeSold) { return false; } if (item.SpawnedInCurrentOutpost) { return false; } if (!item.Prefab.AllowSellingWhenBroken && item.ConditionPercentage < 90.0f) { return false; } - if (confirmedItems.Any(ci => ci.Item == item)) { return false; } + if (confirmedItems != null && confirmedItems.Any(ci => ci.Item == item)) { return false; } if (UndeterminedSoldEntities.TryGetValue(item.Prefab, out int count)) { int newCount = count - 1; @@ -448,13 +451,58 @@ namespace Barotrauma if (containedItems.None()) { return true; } // Allow selling the item if contained items are unsellable and set to be removed on deconstruct if (itemContainer.RemoveContainedItemsOnDeconstruct && containedItems.All(it => !it.Prefab.CanBeSold)) { return true; } - // Otherwise there must be no contained items or the contained items must be confirmed as sold - if (!containedItems.All(it => confirmedItems.Any(ci => ci.Item == it))) { return false; } + if (confirmedItems != null) + { + // Otherwise there must be no contained items or the contained items must be confirmed as sold + if (!containedItems.All(it => confirmedItems.Any(ci => ci.Item == it))) { return false; } + } } return true; } - public static void CreateItems(List itemsToSpawn, Submarine sub) + public static ItemContainer GetOrCreateCargoContainerFor(ItemPrefab item, ISpatialEntity cargoRoomOrSpawnPoint, ref List availableContainers) + { + ItemContainer itemContainer = null; + if (!string.IsNullOrEmpty(item.CargoContainerIdentifier)) + { + itemContainer = availableContainers.Find(ac => + ac.Inventory.CanBePut(item) && + (ac.Item.Prefab.Identifier == item.CargoContainerIdentifier || + ac.Item.Prefab.Tags.Contains(item.CargoContainerIdentifier))); + + if (itemContainer == null) + { + ItemPrefab containerPrefab = ItemPrefab.Prefabs.Find(ep => + ep.Identifier == item.CargoContainerIdentifier || + (ep.Tags != null && ep.Tags.Contains(item.CargoContainerIdentifier))); + + if (containerPrefab == null) + { + DebugConsole.AddWarning($"CargoManager: could not find the item prefab for container {item.CargoContainerIdentifier}!"); + return null; + } + + Vector2 containerPosition = cargoRoomOrSpawnPoint is Hull cargoRoom ? GetCargoPos(cargoRoom, containerPrefab) : cargoRoomOrSpawnPoint.Position; + Item containerItem = new Item(containerPrefab, containerPosition, cargoRoomOrSpawnPoint.Submarine); + itemContainer = containerItem.GetComponent(); + if (itemContainer == null) + { + DebugConsole.AddWarning($"CargoManager: No ItemContainer component found in {containerItem.Prefab.Identifier}!"); + return null; + } + availableContainers.Add(itemContainer); +#if SERVER + if (GameMain.Server != null) + { + Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(itemContainer.Item)); + } +#endif + } + } + return itemContainer; + } + + public static void CreateItems(List itemsToSpawn, Submarine sub, CargoManager cargoManager) { if (itemsToSpawn.Count == 0) { return; } @@ -496,60 +544,26 @@ namespace Barotrauma } List availableContainers = new List(); - ItemPrefab containerPrefab = null; foreach (PurchasedItem pi in itemsToSpawn) { Vector2 position = GetCargoPos(cargoRoom, pi.ItemPrefab); for (int i = 0; i < pi.Quantity; i++) { - ItemContainer itemContainer = null; - if (!string.IsNullOrEmpty(pi.ItemPrefab.CargoContainerIdentifier)) - { - itemContainer = availableContainers.Find(ac => - ac.Inventory.CanBePut(pi.ItemPrefab) && - (ac.Item.Prefab.Identifier == pi.ItemPrefab.CargoContainerIdentifier || - ac.Item.Prefab.Tags.Contains(pi.ItemPrefab.CargoContainerIdentifier.ToLowerInvariant()))); - - if (itemContainer == null) - { - containerPrefab = ItemPrefab.Prefabs.Find(ep => - ep.Identifier == pi.ItemPrefab.CargoContainerIdentifier || - (ep.Tags != null && ep.Tags.Contains(pi.ItemPrefab.CargoContainerIdentifier.ToLowerInvariant()))); - - if (containerPrefab == null) - { - DebugConsole.ThrowError("Cargo spawning failed - could not find the item prefab for container \"" + pi.ItemPrefab.CargoContainerIdentifier + "\"!"); - continue; - } - - Vector2 containerPosition = GetCargoPos(cargoRoom, containerPrefab); - Item containerItem = new Item(containerPrefab, containerPosition, wp.Submarine); - itemContainer = containerItem.GetComponent(); - if (itemContainer == null) - { - DebugConsole.ThrowError("Cargo spawning failed - container \"" + containerItem.Name + "\" does not have an ItemContainer component!"); - continue; - } - availableContainers.Add(itemContainer); -#if SERVER - if (GameMain.Server != null) - { - Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(itemContainer.Item)); - } -#endif - } - } - var item = new Item(pi.ItemPrefab, position, wp.Submarine); - itemContainer?.Inventory.TryPutItem(item, null); - - itemSpawned(item); + 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); #if SERVER Entity.Spawner?.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); #endif (itemContainer?.Item ?? item).CampaignInteractionType = CampaignMode.InteractionType.Cargo; - static void itemSpawned(Item item) + static void itemSpawned(PurchasedItem purchased, Item item) { Submarine sub = item.Submarine ?? item.GetRootContainer()?.Submarine; if (sub != null) @@ -565,6 +579,23 @@ namespace Barotrauma itemsToSpawn.Clear(); } + private readonly List<(PurchasedItem purchaseInfo, IdCard idCard)> purchasedIDCards = new List<(PurchasedItem purchaseInfo, IdCard idCard)>(); + public void InitPurchasedIDCards() + { + foreach ((PurchasedItem purchased, IdCard idCard) in purchasedIDCards) + { + if (idCard != null && purchased.BuyerCharacterInfoIdentifier != 0) + { + var owner = Character.CharacterList.Find(c => c.Info?.GetIdentifier() == purchased.BuyerCharacterInfoIdentifier); + if (owner?.Info != null) + { + var mainSubSpawnPoints = WayPoint.SelectCrewSpawnPoints(new List() { owner.Info }, Submarine.MainSub); + idCard.Initialize(mainSubSpawnPoints.FirstOrDefault(), owner); + } + } + } + } + public static Vector2 GetCargoPos(Hull hull, ItemPrefab itemPrefab) { float floorPos = hull.Rect.Y - hull.Rect.Height; @@ -603,7 +634,7 @@ namespace Barotrauma new XAttribute("id", item.ItemPrefab.Identifier), new XAttribute("qty", item.Quantity), new XAttribute("storeid", storeSpecificItems.Key), - new XAttribute("buyer", item.BuyerCharacterInfoId))); + new XAttribute("buyer", item.BuyerCharacterInfoIdentifier))); } } parentElement.Add(itemsElement); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 6bf837b8b..421af50e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -51,8 +51,6 @@ namespace Barotrauma public ReadyCheck ActiveReadyCheck; - public XElement ActiveOrdersElement { get; set; } - public CrewManager(bool isSinglePlayer) { IsSinglePlayer = isSinglePlayer; @@ -493,9 +491,8 @@ namespace Barotrauma partial void UpdateProjectSpecific(float deltaTime); - private void SaveActiveOrders(XElement parentElement) + public void SaveActiveOrders(XElement element) { - ActiveOrdersElement = new XElement("activeorders"); // Only save orders with no fade out time (e.g. ignore orders) var ordersToSave = new List(); foreach (var activeOrder in ActiveOrders) @@ -504,14 +501,13 @@ namespace Barotrauma if (order == null || activeOrder.FadeOutTime.HasValue) { continue; } ordersToSave.Add(order.WithManualPriority(CharacterInfo.HighestManualOrderPriority)); } - CharacterInfo.SaveOrders(ActiveOrdersElement, ordersToSave.ToArray()); - parentElement?.Add(ActiveOrdersElement); + CharacterInfo.SaveOrders(element, ordersToSave.ToArray()); } - public void LoadActiveOrders() + public void LoadActiveOrders(XElement element) { - if (ActiveOrdersElement == null) { return; } - foreach (var orderInfo in CharacterInfo.LoadOrders(ActiveOrdersElement)) + if (element == null) { return; } + foreach (var orderInfo in CharacterInfo.LoadOrders(element)) { IIgnorable ignoreTarget = null; if (orderInfo.IsIgnoreOrder) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 019a75046..04ca98e30 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; @@ -107,6 +108,8 @@ namespace Barotrauma protected XElement petsElement; + protected XElement ActiveOrdersElement { get; set; } + public CampaignSettings Settings; private readonly List extraMissions = new List(); @@ -739,8 +742,10 @@ namespace Barotrauma foreach (LocationConnection connection in Map.Connections) { connection.Difficulty = MathHelper.Lerp(connection.Difficulty, 100.0f, 0.25f); - connection.LevelData.Difficulty = connection.Difficulty; - connection.LevelData.IsBeaconActive = false; + connection.LevelData = new LevelData(connection) + { + IsBeaconActive = false + }; connection.LevelData.HasHuntingGrounds = connection.LevelData.OriginallyHadHuntingGrounds; } foreach (Location location in Map.Locations) @@ -1032,5 +1037,184 @@ namespace Barotrauma } } + protected void LeaveUnconnectedSubs(Submarine leavingSub) + { + if (leavingSub != Submarine.MainSub && !leavingSub.DockedTo.Contains(Submarine.MainSub)) + { + Submarine.MainSub = leavingSub; + GameMain.GameSession.Submarine = leavingSub; + GameMain.GameSession.SubmarineInfo = leavingSub.Info; + leavingSub.Info.FilePath = System.IO.Path.Combine(SaveUtil.TempPath, leavingSub.Info.Name + ".sub"); + var subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub); + GameMain.GameSession.OwnedSubmarines.Add(leavingSub.Info); + foreach (Submarine sub in subsToLeaveBehind) + { + GameMain.GameSession.OwnedSubmarines.RemoveAll(s => s != leavingSub.Info && s.Name == sub.Info.Name); + MapEntity.mapEntityList.RemoveAll(e => e.Submarine == sub && e is LinkedSubmarine); + LinkedSubmarine.CreateDummy(leavingSub, sub); + } + } + } + + public SubmarineInfo SwitchSubs() + { + TransferItemsBetweenSubs(); + RefreshOwnedSubmarines(); + PendingSubmarineSwitch = null; + return GameMain.GameSession.SubmarineInfo; + } + + /// + /// Also serializes the current sub. + /// + protected void TransferItemsBetweenSubs() + { + Submarine currentSub = GameMain.GameSession.Submarine; + if (currentSub == null || currentSub.Removed) + { + DebugConsole.ThrowError("Cannot transfer items between subs, because the current sub is null or removed!"); + return; + } + var itemsToTransfer = new List<(Item item, Item container)>(); + if (PendingSubmarineSwitch != null) + { + // Remove items from the old sub + foreach (Item item in Item.ItemList) + { + if (item.Removed) { continue; } + if (item.NonInteractable) { continue; } + if (item.HiddenInGame) { continue; } + if (item.Submarine != currentSub) { continue; } + if (item.Prefab.DontTransferBetweenSubs) { continue; } + if (item.GetRootInventoryOwner() is Character) { continue; } + if (item.GetComponent() == null && item.GetComponent() == null && item.GetComponent() == null) { continue; } + if (item.Components.Any(c => c is Holdable h && h.Attached)) { continue; } + if (item.Components.Any(c => c is Wire w && w.Connections.Any(c => c != null))) { continue; } + itemsToTransfer.Add((item, item.Container)); + item.Submarine = null; + } + foreach (var (item, container) in itemsToTransfer) + { + if (container?.Submarine != null) + { + // Drop the item if it's not inside another item set to be transferred. + item.Drop(null, createNetworkEvent: false, setTransform: false); + } + } + } + // Serialize the current sub + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(currentSub); + if (PendingSubmarineSwitch != null && itemsToTransfer.Any()) + { + // Load the new sub + var newSub = new Submarine(PendingSubmarineSwitch); + // Move the transferred items + List availableContainers = Item.ItemList + .Where(it => it.Submarine == newSub && it.HasTag("crate") && !it.NonInteractable && !it.HiddenInGame && !it.Removed) + .Select(it => it.GetComponent()) + .Where(c => c != null) + .ToList(); + foreach (var (item, oldContainer) in itemsToTransfer) + { + Item newContainer = null; + item.Submarine = newSub; + if (item.Container == null) + { + newContainer = newSub.FindContainerFor(item, onlyPrimary: true, checkTransferConditions: true); + } + if (item.Container == null && (newContainer == null || !newContainer.OwnInventory.TryPutItem(item, user: null, createNetworkEvent: false))) + { + WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, newSub); + Hull spawnHull = wp?.CurrentHull ?? Hull.HullList.Where(h => h.Submarine == newSub && !h.IsWetRoom).GetRandomUnsynced(); + if (spawnHull == null) + { + DebugConsole.AddWarning($"Failed to transfer items between subs. No cargo waypoint or dry hulls found in the new sub."); + return; + } + if (spawnHull != null) + { + var cargoContainer = CargoManager.GetOrCreateCargoContainerFor(item.Prefab, spawnHull, ref availableContainers); + if (cargoContainer == null || !cargoContainer.Inventory.TryPutItem(item, user: null, createNetworkEvent: false)) + { + item.SetTransform(wp.SimPosition, 0.0f, findNewHull: false, setPrevTransform: false); + } + } + else + { + DebugConsole.AddWarning($"Failed to transfer item {item.Prefab.Identifier} ({item.ID}), because no cargo spawn point could be found!"); + } + } + string newContainerName = newContainer == null ? "(null)" : $"{newContainer.Prefab.Identifier} ({newContainer.Tags})"; + string msg = "Item transfer log error."; + if (oldContainer != null) + { + if (newContainer == null && oldContainer == item.Container) + { + msg = $"Transferred {item.Prefab.Identifier} ({item.ID}) contained inside {oldContainer.Prefab.Identifier} ({oldContainer.ID})"; + } + else + { + msg = $"Transferred {item.Prefab.Identifier} ({item.ID}) from {oldContainer.Prefab.Identifier} ({oldContainer.Tags}) to {newContainerName}"; + } + } + else + { + msg = $"Transferred {item.Prefab.Identifier} ({item.ID}) to {newContainerName}"; + } +#if DEBUG + DebugConsole.NewMessage(msg); +#else + DebugConsole.Log(msg); +#endif + } + // Serialize the new sub + PendingSubmarineSwitch = new SubmarineInfo(newSub); + } + } + + protected void RefreshOwnedSubmarines() + { + if (PendingSubmarineSwitch != null) + { + SubmarineInfo previousSub = GameMain.GameSession.SubmarineInfo; + GameMain.GameSession.SubmarineInfo = PendingSubmarineSwitch; + + for (int i = 0; i < GameMain.GameSession.OwnedSubmarines.Count; i++) + { + if (GameMain.GameSession.OwnedSubmarines[i].Name == previousSub.Name) + { + GameMain.GameSession.OwnedSubmarines[i] = previousSub; + break; + } + } + } + } + + public void SavePets(XElement parentElement = null) + { + petsElement = new XElement("pets"); + PetBehavior.SavePets(petsElement); + parentElement?.Add(petsElement); + } + + public void LoadPets() + { + if (petsElement != null) + { + PetBehavior.LoadPets(petsElement); + } + } + + public void SaveActiveOrders(XElement parentElement = null) + { + ActiveOrdersElement = new XElement("activeorders"); + CrewManager?.SaveActiveOrders(ActiveOrdersElement); + parentElement?.Add(ActiveOrdersElement); + } + + public void LoadActiveOrders() + { + CrewManager?.LoadActiveOrders(ActiveOrdersElement); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index a8c7034ef..181c73232 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -155,7 +155,7 @@ namespace Barotrauma case "bots" when GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer: CrewManager.HasBots = subElement.GetAttributeBool("hasbots", false); CrewManager.AddCharacterElements(subElement); - CrewManager.ActiveOrdersElement = subElement.GetChildElement("activeorders"); + ActiveOrdersElement = subElement.GetChildElement("activeorders"); break; case "cargo": CargoManager?.LoadPurchasedItems(subElement); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index a7b79214f..9abdd2721 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -275,7 +275,7 @@ namespace Barotrauma /// /// Switch to another submarine. The sub is loaded when the next round starts. /// - public SubmarineInfo SwitchSubmarine(SubmarineInfo newSubmarine, int cost, Client? client = null) + public void SwitchSubmarine(SubmarineInfo newSubmarine, int cost, Client? client = null) { if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name)) { @@ -293,15 +293,12 @@ namespace Barotrauma } } } - if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && cost > 0) { Campaign!.TryPurchase(client, cost); } GameAnalyticsManager.AddMoneySpentEvent(cost, GameAnalyticsManager.MoneySink.SubmarineSwitch, newSubmarine.Name); Campaign!.PendingSubmarineSwitch = newSubmarine; - - return newSubmarine; } public void PurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null) @@ -600,10 +597,13 @@ namespace Barotrauma { //only place items and corpses here in single player //the server does this after loading the respawn shuttle - Level?.SpawnNPCs(); - Level?.SpawnCorpses(); - Level?.PrepareBeaconStation(); - AutoItemPlacer.PlaceIfNeeded(); + if (Level != null) + { + Level.SpawnNPCs(); + Level.SpawnCorpses(); + Level.PrepareBeaconStation(); + } + AutoItemPlacer.SpawnItems(); } if (GameMode is MultiPlayerCampaign mpCampaign) { @@ -836,6 +836,11 @@ namespace Barotrauma { GUI.TogglePauseMenu(); } + if (IsTabMenuOpen) + { + ToggleTabMenu(); + } + GUI.PreventPauseMenuToggle = true; if (!(GameMode is TestGameMode) && Screen.Selected == GameMain.GameScreen && RoundSummary != null) @@ -1072,8 +1077,21 @@ namespace Barotrauma rootElement.Add(new XAttribute("savetime", ToolBox.Epoch.NowLocal)); rootElement.Add(new XAttribute("version", GameMain.Version)); - var submarineInfo = Campaign?.PendingSubmarineSwitch ?? SubmarineInfo; - rootElement.Add(new XAttribute("submarine", submarineInfo == null ? "" : submarineInfo.Name)); + if (Submarine?.Info != null && !Submarine.Removed && Campaign != null) + { + bool hasNewPendingSub = Campaign.PendingSubmarineSwitch != null && + Campaign.PendingSubmarineSwitch.MD5Hash.StringRepresentation != Submarine.Info.MD5Hash.StringRepresentation; + + if (hasNewPendingSub) + { + Campaign.SwitchSubs(); + } + else + { + SubmarineInfo = new SubmarineInfo(Submarine); + } + } + rootElement.Add(new XAttribute("submarine", SubmarineInfo == null ? "" : SubmarineInfo.Name)); if (OwnedSubmarines != null) { List ownedSubmarineNames = new List(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs index fcac2eba3..493ef435d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs @@ -17,7 +17,6 @@ namespace Barotrauma Deselect, Shoot, Command, - ToggleInventory, TakeOneFromInventorySlot, TakeHalfFromInventorySlot, NextFireMode, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index f8249369e..d1371c9fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -99,8 +99,8 @@ namespace Barotrauma.Items.Components { if (!docked && value) { - if (DockingTarget == null) AttemptDock(); - if (DockingTarget == null) return; + if (DockingTarget == null) { AttemptDock(); } + if (DockingTarget == null) { return; } docked = true; } @@ -126,6 +126,14 @@ namespace Barotrauma.Items.Components /// public event Action OnUnDocked; + private bool outpostAutoDockingPromptShown; + + enum AllowOutpostAutoDocking + { + Ask, Yes, No + } + private AllowOutpostAutoDocking allowOutpostAutoDocking = AllowOutpostAutoDocking.Ask; + public DockingPort(Item item, ContentXElement element) : base(item, element) { @@ -622,7 +630,8 @@ namespace Barotrauma.Items.Components { bodies[i + j * 2] = GameMain.World.CreateEdge( ConvertUnits.ToSimUnits(new Vector2(hullRects[i].X, hullRects[i].Y - hullRects[i].Height * j)), - ConvertUnits.ToSimUnits(new Vector2(hullRects[i].Right, hullRects[i].Y - hullRects[i].Height * j))); + ConvertUnits.ToSimUnits(new Vector2(hullRects[i].Right, hullRects[i].Y - hullRects[i].Height * j)), + BodyType.Static); } } @@ -632,7 +641,9 @@ namespace Barotrauma.Items.Components ConvertUnits.ToSimUnits(hullRects[0].Width + hullRects[1].Width), ConvertUnits.ToSimUnits(hullRects[0].Height), density: 0.0f, - offset: ConvertUnits.ToSimUnits(new Vector2(hullRects[0].Right, hullRects[0].Y - hullRects[0].Height / 2) - hulls[0].Submarine.HiddenSubPosition)); + offset: ConvertUnits.ToSimUnits(new Vector2(hullRects[0].Right, hullRects[0].Y - hullRects[0].Height / 2) - hulls[0].Submarine.HiddenSubPosition), + Physics.CollisionWall, + Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionCharacter | Physics.CollisionItemBlocking | Physics.CollisionProjectile); outsideBlocker.UserData = this; } @@ -742,7 +753,8 @@ namespace Barotrauma.Items.Components { bodies[i + j * 2] = GameMain.World.CreateEdge( ConvertUnits.ToSimUnits(new Vector2(hullRects[i].X + hullRects[i].Width * j, hullRects[i].Y)), - ConvertUnits.ToSimUnits(new Vector2(hullRects[i].X + hullRects[i].Width * j, hullRects[i].Y - hullRects[i].Height))); + ConvertUnits.ToSimUnits(new Vector2(hullRects[i].X + hullRects[i].Width * j, hullRects[i].Y - hullRects[i].Height)), + BodyType.Static); } } @@ -752,7 +764,9 @@ namespace Barotrauma.Items.Components ConvertUnits.ToSimUnits(hullRects[0].Width), ConvertUnits.ToSimUnits(hullRects[0].Height + hullRects[1].Height), density: 0.0f, - offset: ConvertUnits.ToSimUnits(new Vector2(hullRects[0].Center.X, hullRects[0].Y) - hulls[0].Submarine.HiddenSubPosition)); + offset: ConvertUnits.ToSimUnits(new Vector2(hullRects[0].Center.X, hullRects[0].Y) - hulls[0].Submarine.HiddenSubPosition), + Physics.CollisionWall, + Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionCharacter | Physics.CollisionItemBlocking | Physics.CollisionProjectile); outsideBlocker.UserData = this; } @@ -778,8 +792,6 @@ namespace Barotrauma.Items.Components if (body == null) { continue; } body.BodyType = BodyType.Static; body.Friction = 0.5f; - - body.CollisionCategories = Physics.CollisionWall; } } @@ -947,7 +959,7 @@ namespace Barotrauma.Items.Components { foreach (Body body in bodies) { - if (body == null) continue; + if (body == null) { continue; } GameMain.World.Remove(body); } bodies = null; @@ -961,6 +973,9 @@ namespace Barotrauma.Items.Components { item.CreateServerEvent(this); } +#elif CLIENT + autodockingVerification?.Close(); + autodockingVerification = null; #endif OnUnDocked?.Invoke(); OnUnDocked = null; @@ -1140,27 +1155,86 @@ namespace Barotrauma.Items.Components public override void ReceiveSignal(Signal signal, Connection connection) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } +#if CLIENT + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && + !(GameMain.GameSession?.Campaign?.AllowedToManageCampaign(ClientPermissions.ManageMap) ?? false)) + { + return; + } +#endif if (dockingCooldown > 0.0f) { return; } bool wasDocked = docked; DockingPort prevDockingTarget = DockingTarget; + bool newDockedState = wasDocked; switch (connection.Name) { case "toggle": if (signal.value != "0") { - Docked = !docked; + newDockedState = !docked; } break; case "set_active": case "set_state": - Docked = signal.value != "0"; + newDockedState = signal.value != "0"; break; } + if (newDockedState != wasDocked) + { + bool tryingToToggleOutpostDocking = docked ? + DockingTarget?.Item?.Submarine?.Info?.IsOutpost ?? false : + FindAdjacentPort()?.Item?.Submarine?.Info?.IsOutpost ?? false; + //trying to dock/undock from an outpost and the signal was sent by some automated system instead of a character + // -> ask if the player really wants to dock/undock to prevent a softlock if someone's wired the docking port + // in a way that makes always makes it dock/undock immediately at the start of the roun + if (tryingToToggleOutpostDocking && signal.sender == null) + { + if (allowOutpostAutoDocking == AllowOutpostAutoDocking.Ask) + { +#if CLIENT + if (!outpostAutoDockingPromptShown) + { + autodockingVerification = new GUIMessageBox(string.Empty, + TextManager.Get(newDockedState ? "autodockverification" : "autoundockverification"), + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); + autodockingVerification.Buttons[0].OnClicked += (btn, userdata) => + { + autodockingVerification?.Close(); + autodockingVerification = null; + if (item.Removed || GameMain.Client == null) { return false; } + allowOutpostAutoDocking = AllowOutpostAutoDocking.Yes; + item.CreateClientEvent(this); + return true; + }; + autodockingVerification.Buttons[1].OnClicked += (btn, userdata) => + { + autodockingVerification?.Close(); + autodockingVerification = null; + if (item.Removed || GameMain.Client == null) { return false; } + allowOutpostAutoDocking = AllowOutpostAutoDocking.No; + item.CreateClientEvent(this); + return true; + }; + } +#endif + outpostAutoDockingPromptShown = true; + return; + } + else if (allowOutpostAutoDocking == AllowOutpostAutoDocking.No) + { + return; + } + } + + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + + Docked = newDockedState; + } + #if SERVER if (signal.sender != null && docked != wasDocked) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 98dc5114d..bb9c5bb64 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -241,12 +241,14 @@ namespace Barotrauma.Items.Components Body = new PhysicsBody( ConvertUnits.ToSimUnits(Math.Max(doorRect.Width, 1)), ConvertUnits.ToSimUnits(Math.Max(doorRect.Height, 1)), - 0.0f, - 1.5f) + radius: 0.0f, + density: 1.5f, + BodyType.Static, + Physics.CollisionWall, + Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionCharacter | Physics.CollisionItemBlocking | Physics.CollisionProjectile, + findNewContacts: false) { UserData = item, - CollisionCategories = Physics.CollisionWall, - BodyType = BodyType.Static, Friction = 0.5f }; Body.SetTransformIgnoreContacts( @@ -258,11 +260,16 @@ namespace Barotrauma.Items.Components } } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { - base.Move(amount); - - Body?.SetTransform(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f); + if (ignoreContacts) + { + Body?.SetTransformIgnoreContacts(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f); + } + else + { + Body?.SetTransform(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f); + } #if CLIENT UpdateConvexHulls(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 384b88db6..9538b5805 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -164,6 +164,8 @@ namespace Barotrauma.Items.Components public bool SwingWhenAiming { get; set; } [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being used (for example, when firing a weapon or a welding tool).")] public bool SwingWhenUsing { get; set; } + [Editable, Serialize(false, IsPropertySaveable.No)] + public bool DisableHeadRotation { get; set; } [ConditionallyEditable(ConditionallyEditable.ConditionType.Attachable, MinValueFloat = 0.0f, MaxValueFloat = 0.999f, DecimalCount = 3), Serialize(0.55f, IsPropertySaveable.No, description: "Sprite depth that's used when the item is NOT attached to a wall.")] public float SpriteDepthWhenDropped @@ -180,11 +182,12 @@ namespace Barotrauma.Items.Components Pusher = null; if (element.GetAttributeBool("blocksplayers", false)) { - Pusher = new PhysicsBody(item.body.width, item.body.height, item.body.radius, item.body.Density) + Pusher = new PhysicsBody(item.body.width, item.body.height, item.body.radius, + item.body.Density, + BodyType.Dynamic, + Physics.CollisionItemBlocking, + Physics.CollisionCharacter | Physics.CollisionProjectile) { - BodyType = BodyType.Dynamic, - CollidesWith = Physics.CollisionCharacter | Physics.CollisionProjectile, - CollisionCategories = Physics.CollisionItemBlocking, Enabled = false, UserData = this }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs index 0dee7a4a2..0467429ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs @@ -79,11 +79,18 @@ namespace Barotrauma.Items.Components IsActive = true; } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { if (trigger != null && amount.LengthSquared() > 0.00001f) { - trigger.SetTransform(item.SimPosition, 0.0f); + if (ignoreContacts) + { + trigger.SetTransformIgnoreContacts(item.SimPosition, 0.0f); + } + else + { + trigger.SetTransform(item.SimPosition, 0.0f); + } } } @@ -119,17 +126,19 @@ namespace Barotrauma.Items.Components } var body = item.body ?? holdable.Body; - + if (body != null) { - trigger = new PhysicsBody(body.width, body.height, body.radius, body.Density) + trigger = new PhysicsBody(body.width, body.height, body.radius, + body.Density, + BodyType.Static, + Physics.CollisionWall, + Physics.CollisionNone, + findNewContacts: false) { UserData = item }; trigger.FarseerBody.SetIsSensor(true); - trigger.FarseerBody.BodyType = BodyType.Static; - trigger.FarseerBody.CollisionCategories = Physics.CollisionWall; - trigger.FarseerBody.CollidesWith = Physics.CollisionNone; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 353a0b0f6..53a55e033 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -111,8 +111,9 @@ namespace Barotrauma.Items.Components ActivateNearbySleepingCharacters(); reloadTimer = reload; - reloadTimer /= (1f + character.GetStatValue(StatTypes.MeleeAttackSpeed)); - reloadTimer /= (1f + item.GetQualityModifier(Quality.StatType.StrikingSpeedMultiplier)); + reloadTimer /= 1f + character.GetStatValue(StatTypes.MeleeAttackSpeed); + reloadTimer /= 1f + item.GetQualityModifier(Quality.StatType.StrikingSpeedMultiplier); + character.AnimController.LockFlippingUntil = (float)Timing.TotalTime + reloadTimer; item.body.FarseerBody.CollisionCategories = Physics.CollisionProjectile; item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionItemBlocking; @@ -216,6 +217,10 @@ namespace Barotrauma.Items.Components { hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 3f, MathHelper.PiOver4)); ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true); + if (ac.InWater) + { + ac.LockFlippingUntil = (float)Timing.TotalTime + Reload; + } } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index ddd615402..f1651b408 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -71,6 +71,7 @@ namespace Barotrauma.Items.Components //return if someone is already trying to pick the item if (pickTimer > 0.0f) { return false; } if (picker == null || picker.Inventory == null) { return false; } + if (!picker.Inventory.AccessibleWhenAlive && !picker.Inventory.AccessibleByOwner) { return false; } if (PickingTime > 0.0f) { @@ -226,7 +227,7 @@ namespace Barotrauma.Items.Components { foreach (Connection c in connectionPanel.Connections) { - foreach (Wire w in c.Wires) + foreach (Wire w in c.Wires.ToArray()) { if (w == null) continue; w.Item.Drop(character); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs index 971068a95..cadea84be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs @@ -40,7 +40,7 @@ namespace Barotrauma.Items.Components public override bool Use(float deltaTime, Character character = null) { - if (character == null || character.Removed) return false; + if (character == null || character.Removed) { return false; } if (!character.IsKeyDown(InputType.Aim) || character.Stun > 0.0f) { return false; } IsActive = true; @@ -55,12 +55,11 @@ namespace Barotrauma.Items.Components if (UsableIn == UseEnvironment.Water) { return true; } } - Vector2 dir = Vector2.Normalize(character.CursorPosition - character.Position); - //move upwards if the cursor is at the position of the character - if (!MathUtils.IsValid(dir)) dir = Vector2.UnitY; - + Vector2 dir = character.CursorPosition - character.Position; + if (!MathUtils.IsValid(dir)) { return true; } + float length = 200; + dir = dir.ClampLength(length) / length; Vector2 propulsion = dir * Force * character.PropulsionSpeedMultiplier; - if (character.AnimController.InWater && Force > 0.0f) { character.AnimController.TargetMovement = dir; } foreach (Limb limb in character.AnimController.Limbs) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 845ae5efe..dd88b19f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -416,7 +416,7 @@ namespace Barotrauma.Items.Components } } - public virtual void Move(Vector2 amount) { } + public virtual void Move(Vector2 amount, bool ignoreContacts = false) { } /// a Character has picked the item public virtual bool Pick(Character picker) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 891fc3fce..3d5f79b0f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -315,7 +315,7 @@ namespace Barotrauma.Items.Components IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null); } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { SetContainedItemPositions(); } @@ -751,7 +751,11 @@ namespace Barotrauma.Items.Components return; } #endif - Inventory.AllItemsMod.ForEach(it => it.Drop(null)); + //if we're unloading the whole sub, no need to drop anything (everything's going to be removed anyway) + if (!Submarine.Unloading) + { + Inventory.AllItemsMod.ForEach(it => it.Drop(null)); + } } public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 296277c9d..4bf9d88fe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -462,19 +462,8 @@ namespace Barotrauma.Items.Components { dir = dir == Direction.Left ? Direction.Right : Direction.Left; } - - userPos.X = -UserPos.X; - - for (int i = 0; i < limbPositions.Count; i++) - { - float diff = (item.Rect.X + limbPositions[i].Position.X * item.Scale) - item.Rect.Center.X; - - Vector2 flippedPos = - new Vector2( - (item.Rect.Center.X - diff - item.Rect.X) / item.Scale, - limbPositions[i].Position.Y); - limbPositions[i] = new LimbPos(limbPositions[i].LimbType, flippedPos, limbPositions[i].AllowUsingLimb); - } + userPos.X = -UserPos.X; + FlipLimbPositions(); } public override void FlipY(bool relativeToSub) @@ -519,6 +508,11 @@ namespace Barotrauma.Items.Components { if (Screen.Selected == GameMain.SubEditorScreen) { + if (item.FlippedX) + { + FlipLimbPositions(); + } + // Don't save flipped positions. foreach (var limbPos in limbPositions) { element.Add(new XElement("limbposition", @@ -526,6 +520,10 @@ namespace Barotrauma.Items.Components new XAttribute("position", XMLExtensions.Vector2ToString(limbPos.Position)), new XAttribute("allowusinglimb", limbPos.AllowUsingLimb))); } + if (item.FlippedX) + { + FlipLimbPositions(); + } } return element; } @@ -558,5 +556,29 @@ namespace Barotrauma.Items.Components } } } + + private void FlipLimbPositions() + { + for (int i = 0; i < limbPositions.Count; i++) + { + float diff = (item.Rect.X + limbPositions[i].Position.X * item.Scale) - item.Rect.Center.X; + + Vector2 flippedPos = + new Vector2( + (item.Rect.Center.X - diff - item.Rect.X) / item.Scale, + limbPositions[i].Position.Y); + limbPositions[i] = new LimbPos(limbPositions[i].LimbType, flippedPos, limbPositions[i].AllowUsingLimb); + } + } + + public override void Reset() + { + base.Reset(); + LoadLimbPositions(originalElement); + if (item.FlippedX) + { + FlipLimbPositions(); + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index d22c343b5..977dda461 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -150,7 +150,7 @@ namespace Barotrauma.Items.Components { if (powerOut?.Grid != null) { return powerOut.Grid.Voltage; } } - return voltage; + return currPowerConsumption <= 0.0f ? 1.0f : voltage; } set { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index fe52d7480..ee6516ef8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -231,6 +231,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, IsPropertySaveable.No, description:"Enable only if you want to make the projectile ignore collisions with other projectiles when it's shot. Doesn't have any effect, if the item is not set to be damaged by projectiles.")] + public bool IgnoreProjectilesWhileActive + { + get; + set; + } + public Body StickTarget { get; @@ -405,6 +412,10 @@ namespace Barotrauma.Items.Components item.body.CollisionCategories = Physics.CollisionProjectile; item.body.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking; + if (item.Prefab.DamagedByProjectiles && !IgnoreProjectilesWhileActive) + { + item.body.CollidesWith |= Physics.CollisionProjectile; + } IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/AndComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/AndComponent.cs new file mode 100644 index 000000000..e98614a3a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/AndComponent.cs @@ -0,0 +1,10 @@ +namespace Barotrauma.Items.Components +{ + sealed class AndComponent : BooleanOperatorComponent + { + public AndComponent(Item item, ContentXElement element) + : base(item, element) { } + + protected override bool GetOutput(int numTrueInputs) => numTrueInputs >= 2; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/BooleanOperatorComponent.cs similarity index 90% rename from Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs rename to Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/BooleanOperatorComponent.cs index 22ceb46ec..bd3140234 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/BooleanOperatorComponent.cs @@ -3,7 +3,7 @@ using System.Xml.Linq; namespace Barotrauma.Items.Components { - class AndComponent : ItemComponent + abstract class BooleanOperatorComponent : ItemComponent { protected string output, falseOutput; @@ -70,22 +70,25 @@ namespace Barotrauma.Items.Components } } - public AndComponent(Item item, ContentXElement element) + public BooleanOperatorComponent(Item item, ContentXElement element) : base(item, element) { timeSinceReceived = new float[] { Math.Max(timeFrame * 2.0f, 0.1f), Math.Max(timeFrame * 2.0f, 0.1f) }; IsActive = true; } - public override void Update(float deltaTime, Camera cam) + protected abstract bool GetOutput(int numTrueInputs); + + public sealed override void Update(float deltaTime, Camera cam) { - bool state = true; + int receivedInputs = 0; for (int i = 0; i < timeSinceReceived.Length; i++) { - if (timeSinceReceived[i] > timeFrame) { state = false; } + if (timeSinceReceived[i] <= timeFrame) { receivedInputs += 1; } timeSinceReceived[i] += deltaTime; } + bool state = GetOutput(receivedInputs); string signalOut = state ? output : falseOutput; if (string.IsNullOrEmpty(signalOut)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/OrComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/OrComponent.cs new file mode 100644 index 000000000..f7208e8e3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/OrComponent.cs @@ -0,0 +1,10 @@ +namespace Barotrauma.Items.Components +{ + sealed class OrComponent : BooleanOperatorComponent + { + public OrComponent(Item item, ContentXElement element) + : base(item, element) { } + + protected override bool GetOutput(int numTrueInputs) => numTrueInputs > 0; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/XorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/XorComponent.cs new file mode 100644 index 000000000..ee299d837 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/BooleanOperatorComponent/XorComponent.cs @@ -0,0 +1,10 @@ +namespace Barotrauma.Items.Components +{ + sealed class XorComponent : BooleanOperatorComponent + { + public XorComponent(Item item, ContentXElement element) + : base(item, element) { } + + protected override bool GetOutput(int numTrueInputs) => numTrueInputs == 1; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index 51fcd38d9..34256fe0c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -19,11 +20,8 @@ namespace Barotrauma.Items.Components public readonly string Name; public readonly LocalizedString DisplayName; - private readonly Wire[] wires; - public IEnumerable Wires - { - get { return wires; } - } + private readonly HashSet wires; + public IReadOnlyCollection Wires => wires; private readonly Item item; @@ -31,7 +29,7 @@ namespace Barotrauma.Items.Components public readonly List Effects; - public readonly ushort[] wireId; + public readonly List LoadedWireIds; //The grid the connection is a part of public GridInfo Grid; @@ -92,7 +90,7 @@ namespace Barotrauma.Items.Components MaxWires = Math.Max(element.Elements().Count(e => e.Name.ToString().Equals("link", StringComparison.OrdinalIgnoreCase)), MaxWires); MaxPlayerConnectableWires = element.GetAttributeInt("maxplayerconnectablewires", MaxWires); - wires = new Wire[MaxWires]; + wires = new HashSet(); IsOutput = element.Name.ToString() == "output"; Name = element.GetAttributeString("name", IsOutput ? "output" : "input"); @@ -150,23 +148,15 @@ namespace Barotrauma.Items.Components IsPower = Name == "power_in" || Name == "power" || Name == "power_out"; - wireId = new ushort[MaxWires]; - + LoadedWireIds = new List(); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "link": - int index = -1; - for (int i = 0; i < MaxWires; i++) - { - if (wireId[i] < 1) { index = i; } - } - if (index == -1) { break; } - int id = subElement.GetAttributeInt("w", 0); if (id < 0) { id = 0; } - wireId[index] = idRemap.GetOffsetId(id); + if (LoadedWireIds.Count < MaxWires) { LoadedWireIds.Add(idRemap.GetOffsetId(id)); } break; case "statuseffect": @@ -185,138 +175,111 @@ namespace Barotrauma.Items.Components private void RefreshRecipients() { recipients.Clear(); - for (int i = 0; i < MaxWires; i++) + foreach (var wire in wires) { - if (wires[i] == null) continue; - Connection recipient = wires[i].OtherConnection(this); - if (recipient != null) recipients.Add(recipient); + Connection recipient = wire.OtherConnection(this); + if (recipient != null) { recipients.Add(recipient); } } recipientsDirty = false; } - public int FindEmptyIndex() - { - for (int i = 0; i < MaxWires; i++) - { - if (wires[i] == null) return i; - } - return -1; - } - - public int FindWireIndex(Wire wire) - { - for (int i = 0; i < MaxWires; i++) - { - if (wires[i] == wire) return i; - } - return -1; - } - - public int FindWireIndex(Item wireItem) - { - for (int i = 0; i < MaxWires; i++) - { - if (wires[i] == null && wireItem == null) return i; - if (wires[i] != null && wires[i].Item == wireItem) return i; - } - return -1; - } + public Wire FindWireByItem(Item it) + => Wires.FirstOrDefault(w => w.Item == it); + public bool WireSlotsAvailable() + => wires.Count < MaxWires; + public bool TryAddLink(Wire wire) { - for (int i = 0; i < MaxWires; i++) + if (wire is null + || wires.Contains(wire) + || !WireSlotsAvailable()) { - if (wires[i] == null) - { - SetWire(i, wire); - return true; - } + return false; } - return false; + wires.Add(wire); + return true; } - public void SetWire(int index, Wire wire) + public void DisconnectWire(Wire wire) { - Wire previousWire = wires[index]; - if (wire != previousWire && previousWire != null) - { - var otherConnection = previousWire.OtherConnection(this); - if (otherConnection != null) - { - //Change the connection grids or flag them for updating - if (IsPower && otherConnection.IsPower && Grid != null) - { - //Check if both connections belong to a larger grid - if (otherConnection.recipients.Count > 1 && recipients.Count > 1) - { - Powered.ChangedConnections.Add(otherConnection); - Powered.ChangedConnections.Add(this); - } - else if (recipients.Count > 1) - { - //This wire was the only one at the other grid - otherConnection.Grid?.RemoveConnection(otherConnection); - otherConnection.Grid = null; - } - else if (otherConnection.recipients.Count > 1) - { - Grid?.RemoveConnection(this); - Grid = null; - } - else if (Grid.Connections.Count == 2) - { - //Delete the grid as these were the only 2 devices - Powered.Grids.Remove(Grid.ID); - Grid = null; - otherConnection.Grid = null; - } - } - otherConnection.recipientsDirty = true; - } - } + if (wire == null || !wires.Contains(wire)) { return; } - wires[index] = wire; + var prevOtherConnection = wire.OtherConnection(this); + if (prevOtherConnection != null) + { + //Change the connection grids or flag them for updating + if (IsPower && prevOtherConnection.IsPower && Grid != null) + { + //Check if both connections belong to a larger grid + if (prevOtherConnection.recipients.Count > 1 && recipients.Count > 1) + { + Powered.ChangedConnections.Add(prevOtherConnection); + Powered.ChangedConnections.Add(this); + } + else if (recipients.Count > 1) + { + //This wire was the only one at the other grid + prevOtherConnection.Grid?.RemoveConnection(prevOtherConnection); + prevOtherConnection.Grid = null; + } + else if (prevOtherConnection.recipients.Count > 1) + { + Grid?.RemoveConnection(this); + Grid = null; + } + else if (Grid.Connections.Count == 2) + { + //Delete the grid as these were the only 2 devices + Powered.Grids.Remove(Grid.ID); + Grid = null; + prevOtherConnection.Grid = null; + } + } + prevOtherConnection.recipientsDirty = true; + } + wires.Remove(wire); recipientsDirty = true; - if (wire != null) + } + + public void ConnectWire(Wire wire) + { + if (wire == null || !TryAddLink(wire)) { return; } + ConnectionPanel.DisconnectedWires.Remove(wire); + var otherConnection = wire.OtherConnection(this); + if (otherConnection != null) { - - ConnectionPanel.DisconnectedWires.Remove(wire); - var otherConnection = wire.OtherConnection(this); - if (otherConnection != null) + //Set the other connection grid if a grid exists already + if (Powered.ValidPowerConnection(this, otherConnection)) { - //Set the other connection grid if a grid exists already - if (Powered.ValidPowerConnection(this, otherConnection)) + if (Grid == null && otherConnection.Grid != null) { - if (Grid == null && otherConnection.Grid != null) - { - otherConnection.Grid.AddConnection(this); - Grid = otherConnection.Grid; - } - else if (Grid != null && otherConnection.Grid == null) - { - Grid.AddConnection(otherConnection); - otherConnection.Grid = Grid; - } - else - { - //Flag change so that proper grids can be formed - Powered.ChangedConnections.Add(this); - Powered.ChangedConnections.Add(otherConnection); - } + otherConnection.Grid.AddConnection(this); + Grid = otherConnection.Grid; + } + else if (Grid != null && otherConnection.Grid == null) + { + Grid.AddConnection(otherConnection); + otherConnection.Grid = Grid; + } + else + { + //Flag change so that proper grids can be formed + Powered.ChangedConnections.Add(this); + Powered.ChangedConnections.Add(otherConnection); } - - otherConnection.recipientsDirty = true; } + + otherConnection.recipientsDirty = true; } + recipientsDirty = true; } public void SendSignal(Signal signal) { - for (int i = 0; i < MaxWires; i++) + foreach (var wire in wires) { - if (wires[i] == null) { continue; } - - Connection recipient = wires[i].OtherConnection(this); + Connection recipient = wire.OtherConnection(this); if (recipient == null) { continue; } if (recipient.item == this.item || signal.source?.LastSentSignalRecipients.LastOrDefault() == recipient) { continue; } @@ -350,35 +313,32 @@ namespace Barotrauma.Items.Components } } - for (int i = 0; i < MaxWires; i++) + foreach (var wire in wires) { - if (wires[i] == null) continue; - - wires[i].RemoveConnection(this); - wires[i] = null; + wire.RemoveConnection(this); recipientsDirty = true; } + wires.Clear(); } - public void ConnectLinked() + public void InitializeFromLoaded() { - if (wireId == null) return; + if (LoadedWireIds.Count == 0) { return; } - for (int i = 0; i < MaxWires; i++) + for (int i = 0; i < LoadedWireIds.Count; i++) { - if (wireId[i] == 0) { continue; } + if (!(Entity.FindEntityByID(LoadedWireIds[i]) is Item wireItem)) { continue; } - if (!(Entity.FindEntityByID(wireId[i]) is Item wireItem)) { continue; } - wires[i] = wireItem.GetComponent(); - recipientsDirty = true; - - if (wires[i] != null) + var wire = wireItem.GetComponent(); + if (wire != null && TryAddLink(wire)) { - if (wires[i].Item.body != null) wires[i].Item.body.Enabled = false; - wires[i].Connect(this, false, false); - wires[i].FixNodeEnds(); + if (wire.Item.body != null) wire.Item.body.Enabled = false; + wire.Connect(this, false, false); + wire.FixNodeEnds(); + recipientsDirty = true; } } + LoadedWireIds.Clear(); } @@ -386,19 +346,10 @@ namespace Barotrauma.Items.Components { XElement newElement = new XElement(IsOutput ? "output" : "input", new XAttribute("name", Name)); - Array.Sort(wires, delegate (Wire wire1, Wire wire2) + foreach (var wire in wires.OrderBy(w => w.Item.ID)) { - if (wire1 == null) return 1; - if (wire2 == null) return -1; - return wire1.Item.ID.CompareTo(wire2.Item.ID); - }); - - for (int i = 0; i < MaxWires; i++) - { - if (wires[i] == null) continue; - newElement.Add(new XElement("link", - new XAttribute("w", wires[i].Item.ID.ToString()))); + new XAttribute("w", wire.Item.ID.ToString()))); } parentElement.Add(newElement); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index f7f7d38eb..41a02b481 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -49,7 +49,7 @@ namespace Barotrauma.Items.Components public bool TemporarilyLocked { - get { return Level.IsLoadedOutpost && item.GetComponent() != null; } + get { return Level.IsLoadedOutpost && (item.GetComponent()?.Docked ?? false); } } //connection panels can't be deactivated externally (by signals or status effects) @@ -99,7 +99,7 @@ namespace Barotrauma.Items.Components { foreach (Connection c in Connections) { - c.ConnectLinked(); + c.InitializeFromLoaded(); } if (disconnectedWireIds != null) @@ -286,25 +286,8 @@ namespace Barotrauma.Items.Components for (int i = 0; i < loadedConnections.Count && i < Connections.Count; i++) { - if (loadedConnections[i].wireId.Length == Connections[i].wireId.Length) - { - loadedConnections[i].wireId.CopyTo(Connections[i].wireId, 0); - } - else - { - //backwards compatibility when maximum number of wires has changed - foreach (ushort id in loadedConnections[i].wireId) - { - for (int j = 0; j < Connections[i].wireId.Length; j++) - { - if (Connections[i].wireId[j] == 0) - { - Connections[i].wireId[j] = id; - break; - } - } - } - } + Connections[i].LoadedWireIds.Clear(); + Connections[i].LoadedWireIds.AddRange(loadedConnections[i].LoadedWireIds); } disconnectedWireIds = element.GetAttributeUshortArray("disconnectedwires", Array.Empty()).ToList(); @@ -361,10 +344,8 @@ namespace Barotrauma.Items.Components DisconnectedWires.Clear(); foreach (Connection c in Connections) { - foreach (Wire wire in c.Wires) + foreach (Wire wire in c.Wires.ToArray()) { - if (wire == null) { continue; } - if (wire.OtherConnection(c) == null) //wire not connected to anything else { #if CLIENT @@ -408,13 +389,14 @@ namespace Barotrauma.Items.Components foreach (Connection connection in Connections) { + msg.WriteVariableUInt32((uint)connection.Wires.Count); foreach (Wire wire in connection.Wires) { msg.Write(wire?.Item == null ? (ushort)0 : wire.Item.ID); } } - msg.Write((ushort)DisconnectedWires.Count()); + msg.Write((ushort)DisconnectedWires.Count); foreach (Wire disconnectedWire in DisconnectedWires) { msg.Write(disconnectedWire.Item.ID); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index df7bc1b0e..fd5ee7f13 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -187,7 +187,7 @@ namespace Barotrauma.Items.Components set; } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { #if CLIENT Light.Position += amount; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs deleted file mode 100644 index 3d3c7ab9d..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Xml.Linq; - -namespace Barotrauma.Items.Components -{ - class OrComponent : AndComponent - { - public OrComponent(Item item, ContentXElement element) - : base(item, element) - { - IsActive = true; - } - - public override void Update(float deltaTime, Camera cam) - { - bool state = false; - for (int i = 0; i < timeSinceReceived.Length; i++) - { - if (timeSinceReceived[i] <= timeFrame) { state = true; } - timeSinceReceived[i] += deltaTime; - } - - string signalOut = state ? output : falseOutput; - if (string.IsNullOrEmpty(signalOut)) - { - //deactivate the component if state is false and there's no false output (will be woken up by non-zero signals in ReceiveSignal) - if (!state) { IsActive = false; } - return; - } - - item.SendSignal(new Signal(signalOut, sender: signalSender[0] ?? signalSender[1]), "signal_out"); - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index c95c55497..1240fb9cb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -135,7 +135,7 @@ namespace Barotrauma.Items.Components // = no point in receiving if (!LinkToChat) { - if (signalOutConnection == null || !signalOutConnection.Wires.Any(w => w != null)) + if (signalOutConnection == null || signalOutConnection.Wires.Count <= 0) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index 19588f114..ec17bd33b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -143,12 +143,11 @@ namespace Barotrauma.Items.Components { if (connections[i] == null || connections[i].Item != item) { continue; } - foreach (Wire wire in connections[i].Wires) + if (connections[i].Wires.Contains(this)) { - if (wire != this) continue; SetConnectedDirty(); - connections[i].SetWire(connections[i].FindWireIndex(wire), null); + connections[i].DisconnectWire(this); } connections[i] = null; @@ -597,15 +596,16 @@ namespace Barotrauma.Items.Components for (int i = 0; i < 2; i++) { if (connections[i] == null) { continue; } - int wireIndex = connections[i].FindWireIndex(item); - if (wireIndex == -1) { continue; } + + var wire = connections[i].FindWireByItem(item); + if (wire is null) { continue; } #if SERVER if (!connections[i].Item.Removed && (!connections[i].Item.Submarine?.Loading ?? true) && (!Level.Loaded?.Generating ?? true)) { connections[i].Item.CreateServerEvent(connections[i].Item.GetComponent()); } #endif - connections[i].SetWire(wireIndex, null); + connections[i].DisconnectWire(wire); connections[i] = null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs deleted file mode 100644 index 71981bb8b..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Xml.Linq; - -namespace Barotrauma.Items.Components -{ - class XorComponent : AndComponent - { - public XorComponent(Item item, ContentXElement element) - : base(item, element) - { - IsActive = true; - } - - public override void Update(float deltaTime, Camera cam) - { - int receivedInputs = 0; - for (int i = 0; i < timeSinceReceived.Length; i++) - { - if (timeSinceReceived[i] <= timeFrame) { receivedInputs += 1; } - timeSinceReceived[i] += deltaTime; - } - - bool state = receivedInputs == 1; - string signalOut = state ? output : falseOutput; - if (string.IsNullOrEmpty(signalOut)) - { - //deactivate the component if state is false and there's no false output (will be woken up by non-zero signals in ReceiveSignal) - if (!state) { IsActive = false; } - return; - } - - item.SendSignal(new Signal(signalOut, sender: signalSender[0] ?? signalSender[1]), "signal_out"); - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs index 582357d8f..5854fccb9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -3,9 +3,8 @@ using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; using Microsoft.Xna.Framework; using System; -using System.Linq; using System.Collections.Generic; -using System.Xml.Linq; +using System.Linq; namespace Barotrauma.Items.Components { @@ -93,13 +92,11 @@ namespace Barotrauma.Items.Components base.OnItemLoaded(); float radiusAttribute = originalElement.GetAttributeFloat("radius", 10.0f); Radius = ConvertUnits.ToSimUnits(radiusAttribute * item.Scale); - PhysicsBody = new PhysicsBody(0.0f, 0.0f, Radius, 1.5f) + PhysicsBody = new PhysicsBody(0.0f, 0.0f, Radius, 1.5f, BodyType.Static, Physics.CollisionWall, LevelTrigger.GetCollisionCategories(triggeredBy)) { - BodyType = BodyType.Static, - CollidesWith = LevelTrigger.GetCollisionCategories(triggeredBy), - CollisionCategories = Physics.CollisionWall, UserData = item }; + PhysicsBody.SetTransformIgnoreContacts(item.SimPosition, 0.0f); PhysicsBody.FarseerBody.SetIsSensor(true); PhysicsBody.FarseerBody.OnCollision += OnCollision; PhysicsBody.FarseerBody.OnSeparation += OnSeparation; @@ -215,12 +212,18 @@ namespace Barotrauma.Items.Components body.ApplyForce(force); } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { - base.Move(amount); if (PhysicsBody != null) { - PhysicsBody.SetTransform(PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f); + if (ignoreContacts) + { + PhysicsBody.SetTransformIgnoreContacts(PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f); + } + else + { + PhysicsBody.SetTransform(PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f); + } PhysicsBody.Submarine = item.Submarine; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 824c85659..a6720fe49 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -661,6 +661,7 @@ namespace Barotrauma.Items.Components while (neededPower > 0.0001f && batteries.Count > 0) { batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f); + if (!batteries.Any()) { break; } float takePower = neededPower / batteries.Count; takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut))); foreach (PowerContainer battery in batteries) @@ -1151,8 +1152,12 @@ namespace Barotrauma.Items.Components foreach (Character enemy in Character.CharacterList) { // Ignore dead, friendly, and those that are inside the same sub - if (enemy.IsDead || !enemy.Enabled || enemy.Submarine == character.Submarine) { continue; } - if (enemy.Submarine != null && enemy.Submarine.TeamID == character.Submarine.TeamID) { continue; } + if (enemy.IsDead || !enemy.Enabled) { continue; } + if (character.Submarine != null) + { + if (enemy.Submarine == character.Submarine) { continue; } + if (enemy.Submarine != null && enemy.Submarine.TeamID == character.Submarine.TeamID) { continue; } + } // Don't aim monsters that are inside any submarine. if (!enemy.IsHuman && enemy.CurrentHull != null) { continue; } if (HumanAIController.IsFriendly(character, enemy)) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 03f8e5d48..8df11b4de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -115,7 +115,7 @@ namespace Barotrauma private readonly Quality qualityComponent; - private readonly ConcurrentQueue impactQueue = new ConcurrentQueue(); + private ConcurrentQueue impactQueue; //a dictionary containing lists of the status effects in all the components of the item private readonly bool[] hasStatusEffectsOfType; @@ -835,33 +835,35 @@ namespace Barotrauma var rand = new Random(ID); density = MathHelper.Lerp(minDensity, maxDensity, (float)rand.NextDouble()); } - body = new PhysicsBody(subElement, ConvertUnits.ToSimUnits(Position), Scale, density); - string collisionCategory = subElement.GetAttributeString("collisioncategory", null); + string collisionCategoryStr = subElement.GetAttributeString("collisioncategory", null); + + Category collisionCategory = Physics.CollisionItem; + Category collidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform; if ((Prefab.DamagedByProjectiles || Prefab.DamagedByMeleeWeapons) && Condition > 0) { //force collision category to Character to allow projectiles and weapons to hit //(we could also do this by making the projectiles and weapons hit CollisionItem //and check if the collision should be ignored in the OnCollision callback, but //that'd make the hit detection more expensive because every item would be included) - body.CollisionCategories = Physics.CollisionCharacter; - body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile; + collisionCategory = Physics.CollisionCharacter; } - if (collisionCategory != null) + if (collisionCategoryStr != null) { - if (!Physics.TryParseCollisionCategory(collisionCategory, out Category cat)) + if (!Physics.TryParseCollisionCategory(collisionCategoryStr, out Category cat)) { - DebugConsole.ThrowError("Invalid collision category in item \"" + Name+"\" (" + collisionCategory + ")"); + DebugConsole.ThrowError("Invalid collision category in item \"" + Name+"\" (" + collisionCategoryStr + ")"); } else { - body.CollisionCategories = cat; + collisionCategory = cat; if (cat.HasFlag(Physics.CollisionCharacter)) { - body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile; + collisionCategory |= Physics.CollisionProjectile; } } } + body = new PhysicsBody(subElement, ConvertUnits.ToSimUnits(Position), Scale, density, collisionCategory, collidesWith, findNewContacts: false); body.FarseerBody.AngularDamping = subElement.GetAttributeFloat("angulardamping", 0.2f); body.FarseerBody.LinearDamping = subElement.GetAttributeFloat("lineardamping", 0.1f); body.UserData = this; @@ -1261,12 +1263,7 @@ namespace Barotrauma partial void SetActiveSpriteProjSpecific(); - public override void Move(Vector2 amount) - { - Move(amount, ignoreContacts: false); - } - - public void Move(Vector2 amount, bool ignoreContacts) + public override void Move(Vector2 amount, bool ignoreContacts = false) { if (!MathUtils.IsValid(amount)) { @@ -1289,7 +1286,7 @@ namespace Barotrauma } foreach (ItemComponent ic in components) { - ic.Move(amount); + ic.Move(amount, ignoreContacts); } if (body != null && (Submarine == null || !Submarine.Loading)) { FindHull(); } @@ -1703,9 +1700,12 @@ namespace Barotrauma public override void Update(float deltaTime, Camera cam) { - while (impactQueue.TryDequeue(out float impact)) + if (impactQueue != null) { - HandleCollision(impact); + while (impactQueue.TryDequeue(out float impact)) + { + HandleCollision(impact); + } } if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && (!Submarine?.Loading ?? true)) @@ -1959,6 +1959,7 @@ namespace Barotrauma if (contact.FixtureA.Body == f1.Body) { normal = -normal; } float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal); + impactQueue ??= new ConcurrentQueue(); impactQueue.Enqueue(impact); return true; @@ -2680,21 +2681,21 @@ namespace Barotrauma foreach (ItemComponent ic in components) { ic.Unequip(character); } } - public List> GetProperties() + public List<(object obj, SerializableProperty property)> GetProperties() { - List> allProperties = new List>(); + List<(object obj, SerializableProperty property)> allProperties = new List<(object obj, SerializableProperty property)>(); List itemProperties = SerializableProperty.GetProperties(this); foreach (var itemProperty in itemProperties) { - allProperties.Add(new Pair(this, itemProperty)); + allProperties.Add((this, itemProperty)); } foreach (ItemComponent ic in components) { List componentProperties = SerializableProperty.GetProperties(ic); foreach (var componentProperty in componentProperties) { - allProperties.Add(new Pair(ic, componentProperty)); + allProperties.Add((ic, componentProperty)); } } return allProperties; @@ -2708,13 +2709,13 @@ namespace Barotrauma SerializableProperty property = extraData.SerializableProperty; if (property != null) { - var propertyOwner = allProperties.Find(p => p.Second == property); + var propertyOwner = allProperties.Find(p => p.property == property); if (allProperties.Count > 1) { - msg.Write((byte)allProperties.FindIndex(p => p.Second == property)); + msg.Write((byte)allProperties.FindIndex(p => p.property == property)); } - object value = property.GetValue(propertyOwner.First); + object value = property.GetValue(propertyOwner.obj); if (value is string stringVal) { msg.Write(stringVal); @@ -2795,7 +2796,7 @@ namespace Barotrauma } } - private List> GetInGameEditableProperties(bool ignoreConditions = false) + private List<(object obj, SerializableProperty property)> GetInGameEditableProperties(bool ignoreConditions = false) { if (ignoreConditions) { @@ -2804,7 +2805,7 @@ namespace Barotrauma else { return GetProperties() - .Where(ce => ce.Second.GetAttribute().IsEditable(this)) + .Where(ce => ce.property.GetAttribute().IsEditable(this)) .Union(GetProperties()).ToList(); } } @@ -2823,8 +2824,8 @@ namespace Barotrauma } bool allowEditing = true; - object parentObject = allProperties[propertyIndex].First; - SerializableProperty property = allProperties[propertyIndex].Second; + object parentObject = allProperties[propertyIndex].obj; + SerializableProperty property = allProperties[propertyIndex].property; if (inGameEditableOnly && parentObject is ItemComponent ic) { if (!ic.AllowInGameEditing) { allowEditing = false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 74532ffa6..e97bb6481 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -253,6 +253,12 @@ namespace Barotrauma public readonly float MinCondition; public readonly int MinAmount; public readonly int MaxAmount; + // Overrides min and max, if defined. + public readonly int Amount; + public readonly bool CampaignOnly; + public readonly bool NotCampaign; + public readonly bool TransferOnlyOnePerContainer; + public readonly bool AllowTransfersHere = true; public PreferredContainer(XElement element) { @@ -261,21 +267,26 @@ namespace Barotrauma SpawnProbability = element.GetAttributeFloat("spawnprobability", 0.0f); MinAmount = element.GetAttributeInt("minamount", 0); MaxAmount = Math.Max(MinAmount, element.GetAttributeInt("maxamount", 0)); + Amount = element.GetAttributeInt("amount", 0); MaxCondition = element.GetAttributeFloat("maxcondition", 100f); MinCondition = element.GetAttributeFloat("mincondition", 0f); + CampaignOnly = element.GetAttributeBool("campaignonly", CampaignOnly); + NotCampaign = element.GetAttributeBool("notcampaign", NotCampaign); + TransferOnlyOnePerContainer = element.GetAttributeBool("TransferOnlyOnePerContainer", TransferOnlyOnePerContainer); + AllowTransfersHere = element.GetAttributeBool("AllowTransfersHere", AllowTransfersHere); - if (element.Attribute("spawnprobability") == null) + if (element.GetAttribute("spawnprobability") == null) { //if spawn probability is not defined but amount is, assume the probability is 1 - if (MaxAmount > 0) + if (MaxAmount > 0 || Amount > 0) { SpawnProbability = 1.0f; } } - else if (element.Attribute("minamount") == null && element.Attribute("maxamount") == null) + else if (element.GetAttribute("minamount") == null && element.GetAttribute("maxamount") == null && element.GetAttribute("amount") == null) { //spawn probability defined but amount isn't, assume amount is 1 - MinAmount = MaxAmount = 1; + MinAmount = MaxAmount = Amount = 1; SpawnProbability = element.GetAttributeFloat("spawnprobability", 0.0f); } } @@ -600,6 +611,9 @@ namespace Barotrauma public ImmutableHashSet AllowDroppingOnSwapWith { get; private set; } + [Serialize(false, IsPropertySaveable.No)] + public bool DontTransferBetweenSubs { get; private set; } + protected override Identifier DetermineIdentifier(XElement element) { Identifier identifier = base.DetermineIdentifier(element); @@ -1084,7 +1098,7 @@ namespace Barotrauma //legacy support identifier = GenerateLegacyIdentifier(name); } - prefab = Find(p => p is ItemPrefab && p.Identifier == identifier) as ItemPrefab; + Prefabs.TryGet(identifier, out prefab); //not found, see if we can find a prefab with a matching alias if (prefab == null && !string.IsNullOrEmpty(name)) @@ -1104,12 +1118,13 @@ namespace Barotrauma return prefab; } - public bool IsContainerPreferred(Item item, ItemContainer targetContainer, out bool isPreferencesDefined, out bool isSecondary, bool requireConditionRequirement = false) + public bool IsContainerPreferred(Item item, ItemContainer targetContainer, out bool isPreferencesDefined, out bool isSecondary, bool requireConditionRequirement = false, bool checkTransferConditions = false) { isPreferencesDefined = PreferredContainers.Any(); isSecondary = false; if (!isPreferencesDefined) { return true; } - if (PreferredContainers.Any(pc => (!requireConditionRequirement || HasConditionRequirement(pc)) && IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Primary, targetContainer))) + if (PreferredContainers.Any(pc => (!requireConditionRequirement || HasConditionRequirement(pc)) && IsItemConditionAcceptable(item, pc) && + IsContainerPreferred(pc.Primary, targetContainer) && (!checkTransferConditions || CanBeTransferred(item.Prefab.Identifier, pc, targetContainer)))) { return true; } @@ -1132,6 +1147,8 @@ namespace Barotrauma } private bool IsItemConditionAcceptable(Item item, PreferredContainer pc) => item.ConditionPercentage >= pc.MinCondition && item.ConditionPercentage <= pc.MaxCondition; + private bool CanBeTransferred(Identifier item, PreferredContainer pc, ItemContainer targetContainer) => + pc.AllowTransfersHere && (!pc.TransferOnlyOnePerContainer || targetContainer.Inventory.AllItems.None(i => i.Prefab.Identifier == item)); public static bool IsContainerPreferred(IEnumerable preferences, ItemContainer c) => preferences.Any(id => c.Item.Prefab.Identifier == id || c.Item.HasTag(id)); public static bool IsContainerPreferred(IEnumerable preferences, IEnumerable ids) => ids.Any(id => preferences.Contains(id)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index d6a3357f0..b5fd3d37c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -214,7 +214,7 @@ namespace Barotrauma.MapCreatures.Behavior [Serialize(400, IsPropertySaveable.Yes, "How much health the root has.")] public int RootHealth { get; set; } - [Serialize(0.0005f, IsPropertySaveable.Yes, "How fast the root's health regenerates per each grown branch.")] + [Serialize(0.00025f, IsPropertySaveable.Yes, "How fast the root's health regenerates per each grown branch.")] public float HealthRegenPerBranch { get; set; } [Serialize(30, IsPropertySaveable.Yes, "How far away from the root branches can regenerate health (in number of branches). The amount of regen decreases lineary further from the root.")] @@ -1148,7 +1148,7 @@ namespace Barotrauma.MapCreatures.Behavior return; } #if SERVER - if (!wasRemoved) + if (!wasRemoved && Parent != null && !Parent.Removed) { CreateNetworkMessage(new BranchRemoveEventData(branch)); } @@ -1199,7 +1199,10 @@ namespace Barotrauma.MapCreatures.Behavior StateMachine?.State?.Exit(); #if SERVER - CreateNetworkMessage(new KillEventData()); + if (Parent != null && !Parent.Removed) + { + CreateNetworkMessage(new KillEventData()); + } #endif } @@ -1220,8 +1223,11 @@ namespace Barotrauma.MapCreatures.Behavior } _entityList.Remove(this); -#if SERVER - CreateNetworkMessage(new RemoveEventData()); +#if SERVER + if (Parent != null && !Parent.Removed) + { + CreateNetworkMessage(new RemoveEventData()); + } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index d0c9e6d0a..53c2a59eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -148,11 +148,12 @@ namespace Barotrauma InsertToList(); float blockerSize = ConvertUnits.ToSimUnits(Math.Max(rect.Width, rect.Height)) / 2; - outsideCollisionBlocker = GameMain.World.CreateEdge(-Vector2.UnitX * blockerSize, Vector2.UnitX * blockerSize); + outsideCollisionBlocker = GameMain.World.CreateEdge(-Vector2.UnitX * blockerSize, Vector2.UnitX * blockerSize, + BodyType.Static, + Physics.CollisionWall, + Physics.CollisionCharacter, + findNewContacts: false); outsideCollisionBlocker.UserData = $"CollisionBlocker (Gap {ID})"; - outsideCollisionBlocker.BodyType = BodyType.Static; - outsideCollisionBlocker.CollisionCategories = Physics.CollisionWall; - outsideCollisionBlocker.CollidesWith = Physics.CollisionCharacter; outsideCollisionBlocker.Enabled = false; #if CLIENT Resized += newRect => IsHorizontal = newRect.Width < newRect.Height; @@ -165,7 +166,7 @@ namespace Barotrauma return new Gap(rect, IsHorizontal, Submarine); } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { if (!MathUtils.IsValid(amount)) { @@ -326,14 +327,6 @@ namespace Barotrauma { lerpedFlowForce = Vector2.Lerp(lerpedFlowForce, flowForce, deltaTime * 5.0f); } - if (FlowTargetHull != null && IsRoomToRoom) - { - var otherRoom = linkedTo[1] == FlowTargetHull ? linkedTo[0] : linkedTo[1]; - if ((otherRoom as Hull).Volume < FlowTargetHull.Volume) - { - lerpedFlowForce = Vector2.Zero; - } - } openedTimer -= deltaTime; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index f4172ff8f..88cfa4d2c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -590,7 +590,7 @@ namespace Barotrauma return index; } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { if (!MathUtils.IsValid(amount)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs index 735066d2d..133da33e1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs @@ -157,9 +157,6 @@ namespace Barotrauma public static List GeneratePath(List targetCells, List cells) { - Stopwatch sw2 = new Stopwatch(); - sw2.Start(); - List pathCells = new List(); if (targetCells.Count == 0) { return pathCells; } @@ -213,10 +210,6 @@ namespace Barotrauma } while (currentCell != targetCells[targetCells.Count - 1] && iterationsLeft > 0); - - Debug.WriteLine("gettooclose: " + sw2.ElapsedMilliseconds + " ms"); - sw2.Restart(); - return pathCells; } @@ -351,7 +344,7 @@ namespace Barotrauma BodyType = BodyType.Static, CollisionCategories = Physics.CollisionLevel }; - GameMain.World.Add(cellBody); + GameMain.World.Add(cellBody, findNewContacts: false); for (int n = cells.Count - 1; n >= 0; n-- ) { @@ -429,7 +422,9 @@ namespace Barotrauma Vertices bodyVertices = new Vertices(triangles[i]); PolygonShape polygon = new PolygonShape(bodyVertices, 5.0f); - Fixture fixture = new Fixture(polygon) + Fixture fixture = new Fixture(polygon, + Physics.CollisionLevel, + Physics.CollisionAll) { UserData = cell }; @@ -446,8 +441,6 @@ namespace Barotrauma } cell.Body = cellBody; } - - cellBody.CollisionCategories = Physics.CollisionLevel; cellBody.ResetMassData(); return cellBody; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 763fb0324..426a8d9ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -299,11 +299,41 @@ namespace Barotrauma /// Random integers generated during the level generation. If these values differ between clients/server, /// it means the levels aren't identical for some reason and there will most likely be major ID mismatches. /// - public List EqualityCheckValues + public enum LevelGenStage { - get; - private set; - } = new List(); + GenStart, + TunnelGen, + VoronoiGen, + VoronoiGen2, + VoronoiGen3, + Ruins, + FloatingIce, + LevelBodies, + IceSpires, + TopAndBottom, + PlaceLevelObjects, + GenerateItems, + Finish + } + + private readonly Dictionary equalityCheckValues = Enum.GetValues(typeof(LevelGenStage)) + .Cast() + .Select(k => (k, 0)) + .ToDictionary(); + public IReadOnlyDictionary EqualityCheckValues => equalityCheckValues; + + private void GenerateEqualityCheckValue(LevelGenStage stage) + { + equalityCheckValues[stage] = Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient); + } + + private void ClearEqualityCheckValues() + { + foreach (LevelGenStage stage in Enum.GetValues(typeof(LevelGenStage))) + { + equalityCheckValues[stage] = 0; + } + } public List EntitiesBeforeGenerate { get; private set; } = new List(); public int EntityCountBeforeGenerate { get; private set; } @@ -404,7 +434,7 @@ namespace Barotrauma Loaded = this; Generating = true; - EqualityCheckValues.Clear(); + ClearEqualityCheckValues(); EntitiesBeforeGenerate = GetEntities().ToList(); EntityCountBeforeGenerate = EntitiesBeforeGenerate.Count(); @@ -414,7 +444,7 @@ namespace Barotrauma EndLocation = GameMain.GameSession?.EndLocation; } - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.GenStart); LevelObjectManager = new LevelObjectManager(); @@ -477,7 +507,7 @@ namespace Barotrauma (int)MathHelper.Lerp(borders.Bottom - Math.Max(minMainPathWidth, ExitDistance * 1.5f), borders.Y + minMainPathWidth, GenerationParams.EndPosition.Y)); endExitPosition = new Point(endPosition.X, borders.Bottom); - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.TunnelGen); //---------------------------------------------------------------------------------- //generate the initial nodes for the main path and smaller tunnels @@ -573,7 +603,7 @@ namespace Barotrauma GenerateAbyssArea(); GenerateCaves(mainPath); - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.VoronoiGen); //---------------------------------------------------------------------------------- //generate voronoi sites @@ -678,7 +708,7 @@ namespace Barotrauma } } - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.VoronoiGen2); //---------------------------------------------------------------------------------- // construct the voronoi graph and cells @@ -796,7 +826,7 @@ namespace Barotrauma startPosition.X = (int)pathCells[0].Site.Coord.X; startExitPosition.X = startPosition.X; - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.VoronoiGen3); //---------------------------------------------------------------------------------- // remove unnecessary cells and create some holes at the bottom of the level @@ -1025,7 +1055,7 @@ namespace Barotrauma } } - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.Ruins); //---------------------------------------------------------------------------------- // create some ruins @@ -1038,7 +1068,7 @@ namespace Barotrauma GenerateRuin(ruinPositions[i], mirror); } - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.FloatingIce); //---------------------------------------------------------------------------------- // create floating ice chunks @@ -1070,7 +1100,7 @@ namespace Barotrauma } } - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.LevelBodies); //---------------------------------------------------------------------------------- // generate the bodies and rendered triangles of the cells @@ -1175,7 +1205,7 @@ namespace Barotrauma } #endif - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.IceSpires); //---------------------------------------------------------------------------------- // create ice spires @@ -1210,7 +1240,7 @@ namespace Barotrauma CreateOutposts(); - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.TopAndBottom); //---------------------------------------------------------------------------------- // top barrier & sea floor @@ -1252,15 +1282,15 @@ namespace Barotrauma CreateWrecks(); CreateBeaconStation(); - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.PlaceLevelObjects); LevelObjectManager.PlaceObjects(this, GenerationParams.LevelObjectAmount); - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.GenerateItems); GenerateItems(); - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); + GenerateEqualityCheckValue(LevelGenStage.Finish); #if CLIENT backgroundCreatureManager.SpawnCreatures(this, GenerationParams.BackgroundCreatureAmount); @@ -2606,7 +2636,7 @@ namespace Barotrauma #if DEBUG DebugConsole.NewMessage("Level resources spawned: " + itemCount + "\n" + " Spawn points containing resources: " + PathPoints.Where(p => p.ClusterLocations.Any()).Count() + "/" + PathPoints.Count + "\n" + - " Total value: "+ PathPoints.Sum(p => p.ClusterLocations.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0)))+" mk"); + " Total value: " + PathPoints.Sum(p => p.ClusterLocations.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0))) + " mk"); if (AbyssResources.Count > 0) { @@ -3688,6 +3718,7 @@ namespace Barotrauma if (wreckFiles.None()) { DebugConsole.ThrowError("No wreck files found in the selected content packages!"); + Wrecks = new List(); return; } wreckFiles.Shuffle(Rand.RandSync.ServerAndClient); @@ -4112,12 +4143,12 @@ namespace Barotrauma int corpseCount = Rand.Range(Loaded.GenerationParams.MinCorpseCount, Loaded.GenerationParams.MaxCorpseCount + 1); var allSpawnPoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == wreck && wp.CurrentHull != null); var pathPoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Path); - pathPoints.Shuffle(Rand.RandSync.Unsynced); var corpsePoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Corpse); - corpsePoints.Shuffle(Rand.RandSync.Unsynced); - if (!corpsePoints.Any() && !pathPoints.Any()) { continue; } - + pathPoints.Shuffle(Rand.RandSync.Unsynced); + // Sort by job so that we first spawn those with a predefined job (might have special id cards) + corpsePoints = corpsePoints.OrderBy(p => p.AssignedJob == null).ThenBy(p => Rand.Value()).ToList(); + var usedJobs = new HashSet(); int spawnCounter = 0; for (int j = 0; j < corpseCount; j++) { @@ -4126,18 +4157,18 @@ namespace Barotrauma CorpsePrefab selectedPrefab; if (job == null) { - selectedPrefab = GetCorpsePrefab(p => p.SpawnPosition == PositionType.Wreck); + selectedPrefab = GetCorpsePrefab(usedJobs); } else { - selectedPrefab = GetCorpsePrefab(p => p.SpawnPosition == PositionType.Wreck && (p.Job == "any" || p.Job == job.Identifier)); + selectedPrefab = GetCorpsePrefab(usedJobs, p => p.Job == "any" || p.Job == job.Identifier); if (selectedPrefab == null) { corpsePoints.Remove(sp); pathPoints.Remove(sp); sp = corpsePoints.FirstOrDefault(sp => sp.AssignedJob == null) ?? pathPoints.FirstOrDefault(sp => sp.AssignedJob == null); // Deduce the job from the selected prefab - selectedPrefab = GetCorpsePrefab(p => p.SpawnPosition == PositionType.Wreck); + selectedPrefab = GetCorpsePrefab(usedJobs); } } if (selectedPrefab == null) { continue; } @@ -4156,28 +4187,65 @@ namespace Barotrauma pathPoints.Remove(sp); } - job ??= selectedPrefab.GetJobPrefab(); + job ??= selectedPrefab.GetJobPrefab(predicate: p => !usedJobs.Contains(p)); if (job == null) { continue; } - + if (job.Identifier == "captain" || job.Identifier == "engineer" || job.Identifier == "medicaldoctor" || job.Identifier == "securityofficer") + { + // Only spawn one of these jobs per wreck + usedJobs.Add(job); + } var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, randSync: Rand.RandSync.ServerAndClient); var corpse = Character.Create(CharacterPrefab.HumanSpeciesName, worldPos, ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); corpse.AnimController.FindHull(worldPos, setSubmarine: true); corpse.TeamID = CharacterTeamType.None; corpse.EnableDespawn = false; selectedPrefab.GiveItems(corpse, wreck); + corpse.CharacterHealth.ApplyAffliction(corpse.AnimController.MainLimb, AfflictionPrefab.OxygenLow.Instantiate(200)); + bool applyBurns = Rand.Value() < 0.1f; + bool applyDamage = Rand.Value() < 0.3f; + foreach (var limb in corpse.AnimController.Limbs) + { + if (applyDamage && (limb.type == LimbType.Head || Rand.Value() < 0.5f)) + { + var prefab = AfflictionPrefab.BiteWounds; + float max = prefab.MaxStrength / prefab.DamageOverlayAlpha; + corpse.CharacterHealth.ApplyAffliction(limb, prefab.Instantiate(GetStrength(limb, max))); + } + if (applyBurns) + { + var prefab = AfflictionPrefab.Burn; + float max = prefab.MaxStrength / prefab.BurnOverlayAlpha; + corpse.CharacterHealth.ApplyAffliction(limb, prefab.Instantiate(GetStrength(limb, max))); + } + + static float GetStrength(Limb limb, float max) + { + float strength = Rand.Range(0, max); + if (limb.type != LimbType.Head) + { + strength = Math.Min(strength, Rand.Range(0, max)); + } + return strength; + } + } corpse.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false); corpse.GiveIdCardTags(sp); -#if SERVER - if (selectedPrefab.MinMoney >= 0 && selectedPrefab.MaxMoney > 0) + + bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true }; + if (isServerOrSingleplayer && selectedPrefab.MinMoney >= 0 && selectedPrefab.MaxMoney > 0) { corpse.Wallet.Give(Rand.Range(selectedPrefab.MinMoney, selectedPrefab.MaxMoney, Rand.RandSync.Unsynced)); } -#endif + spawnCounter++; - static CorpsePrefab GetCorpsePrefab(Func predicate) + static CorpsePrefab GetCorpsePrefab(HashSet usedJobs, Func predicate = null) { - IEnumerable filteredPrefabs = CorpsePrefab.Prefabs.Where(predicate); + IEnumerable filteredPrefabs = CorpsePrefab.Prefabs.Where(p => + usedJobs.None(j => j.Identifier == p.Job.ToIdentifier()) && + p.SpawnPosition == PositionType.Wreck && + (predicate == null || predicate(p))); + return ToolBox.SelectWeightedRandom(filteredPrefabs.ToList(), filteredPrefabs.Select(p => p.Commonness).ToList(), Rand.RandSync.Unsynced); } } @@ -4270,7 +4338,7 @@ namespace Barotrauma blockedRects?.Clear(); EntitiesBeforeGenerate?.Clear(); - EqualityCheckValues?.Clear(); + ClearEqualityCheckValues(); if (Ruins != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 8356a9461..7313759d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -20,7 +20,7 @@ namespace Barotrauma public readonly string Seed; - public float Difficulty; + public readonly float Difficulty; public readonly Biome Biome; @@ -141,8 +141,8 @@ namespace Barotrauma Seed = locationConnection.Locations[0].BaseName + locationConnection.Locations[1].BaseName; Biome = locationConnection.Biome; Type = LevelType.LocationConnection; - GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.LocationConnection, Biome.Identifier); Difficulty = locationConnection.Difficulty; + GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.LocationConnection, Difficulty, Biome.Identifier); float sizeFactor = MathUtils.InverseLerp( MapGenerationParams.Instance.SmallLevelConnectionLength, @@ -171,13 +171,13 @@ namespace Barotrauma /// /// Instantiates level data using the properties of the location /// - public LevelData(Location location) + public LevelData(Location location, float difficulty) { Seed = location.BaseName; Biome = location.Biome; Type = LevelType.Outpost; - GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.Outpost, Biome.Identifier); - Difficulty = 0.0f; + Difficulty = difficulty; + GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.Outpost, Difficulty, Biome.Identifier); var rand = new MTRandom(ToolBox.StringToInt(Seed)); int width = (int)MathHelper.Lerp(GenerationParams.MinWidth, GenerationParams.MaxWidth, (float)rand.NextDouble()); @@ -200,14 +200,16 @@ namespace Barotrauma (requireOutpost ? LevelType.Outpost : LevelType.LocationConnection) : generationParams.Type; - if (generationParams == null) { generationParams = LevelGenerationParams.GetRandom(seed, type); } + float selectedDifficulty = difficulty ?? Rand.Range(30.0f, 80.0f, Rand.RandSync.ServerAndClient); + + if (generationParams == null) { generationParams = LevelGenerationParams.GetRandom(seed, type, selectedDifficulty); } var biome = Biome.Prefabs.FirstOrDefault(b => generationParams?.AllowedBiomeIdentifiers.Contains(b.Identifier) ?? false) ?? Biome.Prefabs.GetRandom(Rand.RandSync.ServerAndClient); var levelData = new LevelData( seed, - difficulty ?? Rand.Range(30.0f, 80.0f, Rand.RandSync.ServerAndClient), + selectedDifficulty, Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient), generationParams, biome); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index cb6640f49..d4e56b2e1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -68,6 +67,27 @@ namespace Barotrauma set; } + [Serialize(1.0f, IsPropertySaveable.Yes, "If there are multiple level generation parameters available for a level in a given biome, their commonness determines how likely it is for one to get selected."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] + public float Commonness + { + get; + set; + } + + [Serialize(0.0f, IsPropertySaveable.Yes, "The difficulty of the level has to be above or equal to this for these parameters to get chosen for the level."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] + public float MinLevelDifficulty + { + get; + set; + } + + [Serialize(100.0f, IsPropertySaveable.Yes, "The difficulty of the level has to be below or equal to this for these parameters to get chosen for the level."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] + public float MaxLevelDifficulty + { + get; + set; + } + [Serialize("27,30,36", IsPropertySaveable.Yes), Editable] public Color AmbientLightColor { @@ -536,7 +556,26 @@ namespace Barotrauma public Sprite WallSpriteDestroyed { get; private set; } public Sprite WaterParticles { get; private set; } - public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, Identifier biome = default) + #warning TODO: this should be in the unit test project (#3164) + public static void CheckValidity() + { + foreach (Biome biome in Biome.Prefabs) + { + for (float i = 0.0f; i <= 100.0f; i += 0.5f) + { + if (GetRandom("test", LevelData.LevelType.LocationConnection, i, biome.Identifier) == null) + { + DebugConsole.ThrowError($"No suitable level generation parameters found for a specific type of level (level type: LocationConnection, difficulty: {i}, biome: {biome.Identifier})"); + } + if (GetRandom("test", LevelData.LevelType.Outpost, i, biome.Identifier) == null) + { + DebugConsole.ThrowError($"No suitable level generation parameters found for a specific type of level (level type: Outpost, difficulty: {i}, biome: {biome.Identifier})"); + } + } + } + } + + public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, float difficulty, Identifier biome = default) { Rand.SetSyncedSeed(ToolBox.StringToInt(seed)); @@ -568,7 +607,16 @@ namespace Barotrauma } } - return matchingLevelParams.GetRandom(Rand.RandSync.ServerAndClient); + if (!matchingLevelParams.Any(lp => difficulty >= lp.MinLevelDifficulty && difficulty <= lp.MaxLevelDifficulty)) + { + DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biome.IfEmpty("null".ToIdentifier())}\", type: \"{type}\", difficulty: {difficulty})"); + } + else + { + matchingLevelParams = matchingLevelParams.Where(lp => difficulty >= lp.MinLevelDifficulty && difficulty <= lp.MaxLevelDifficulty); + } + + return ToolBox.SelectWeightedRandom(matchingLevelParams, p => p.Commonness, Rand.RandSync.ServerAndClient); } public LevelGenerationParams(ContentXElement element, LevelGenerationParametersFile file) : base(file, element.GetAttributeIdentifier("identifier", element.Name.LocalName)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 5fd728e3e..b07e2dd78 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -102,32 +102,44 @@ namespace Barotrauma foreach (Structure structure in Structure.WallList) { if (!structure.HasBody || structure.HiddenInGame) { continue; } + + LevelObjectPrefab.SpawnPosType spawnPosType = LevelObjectPrefab.SpawnPosType.None; if (level.Ruins.Any(r => r.Submarine == structure.Submarine)) { - if (structure.IsHorizontal) - { - bool topHull = Hull.FindHull(structure.WorldPosition + Vector2.UnitY * 64) != null; - bool bottomHull = Hull.FindHull(structure.WorldPosition - Vector2.UnitY * 64) != null; - if (topHull && bottomHull ) { continue; } + spawnPosType = LevelObjectPrefab.SpawnPosType.RuinWall; + } + else if (structure.Submarine?.Info?.Type == SubmarineType.Outpost) + { + spawnPosType = LevelObjectPrefab.SpawnPosType.OutpostWall; + } + else + { + continue; + } - availableSpawnPositions.Add(new SpawnPosition( - new GraphEdge(new Vector2(structure.WorldRect.X, structure.WorldPosition.Y), new Vector2(structure.WorldRect.Right, structure.WorldPosition.Y)), - bottomHull ? Vector2.UnitY : -Vector2.UnitY, - LevelObjectPrefab.SpawnPosType.RuinWall, - bottomHull ? Alignment.Bottom : Alignment.Top)); - } - else - { - bool rightHull = Hull.FindHull(structure.WorldPosition + Vector2.UnitX * 64) != null; - bool leftHull = Hull.FindHull(structure.WorldPosition - Vector2.UnitX * 64) != null; - if (rightHull && leftHull) { continue; } + if (structure.IsHorizontal) + { + bool topHull = Hull.FindHull(structure.WorldPosition + Vector2.UnitY * 64) != null; + bool bottomHull = Hull.FindHull(structure.WorldPosition - Vector2.UnitY * 64) != null; + if (topHull && bottomHull) { continue; } - availableSpawnPositions.Add(new SpawnPosition( - new GraphEdge(new Vector2(structure.WorldPosition.X, structure.WorldRect.Y), new Vector2(structure.WorldPosition.X, structure.WorldRect.Y - structure.WorldRect.Height)), - leftHull ? Vector2.UnitX : -Vector2.UnitX, - LevelObjectPrefab.SpawnPosType.RuinWall, - leftHull ? Alignment.Left : Alignment.Right)); - } + availableSpawnPositions.Add(new SpawnPosition( + new GraphEdge(new Vector2(structure.WorldRect.X, structure.WorldPosition.Y), new Vector2(structure.WorldRect.Right, structure.WorldPosition.Y)), + bottomHull ? Vector2.UnitY : -Vector2.UnitY, + spawnPosType, + bottomHull ? Alignment.Bottom : Alignment.Top)); + } + else + { + bool rightHull = Hull.FindHull(structure.WorldPosition + Vector2.UnitX * 64) != null; + bool leftHull = Hull.FindHull(structure.WorldPosition - Vector2.UnitX * 64) != null; + if (rightHull && leftHull) { continue; } + + availableSpawnPositions.Add(new SpawnPosition( + new GraphEdge(new Vector2(structure.WorldPosition.X, structure.WorldRect.Y), new Vector2(structure.WorldPosition.X, structure.WorldRect.Y - structure.WorldRect.Height)), + leftHull ? Vector2.UnitX : -Vector2.UnitX, + spawnPosType, + leftHull ? Alignment.Left : Alignment.Right)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs index 9917943c1..326b443ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs @@ -44,6 +44,7 @@ namespace Barotrauma MainPath = 64, LevelStart = 128, LevelEnd = 256, + OutpostWall = 512, Wall = MainPathWall | SidePathWall | CaveWall, } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index 02140f9a5..480163ffa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -6,7 +6,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index f39a30ee4..e17f274bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml.Linq; -using StoreBalanceStatus = Barotrauma.LocationType.StoreBalanceStatus; namespace Barotrauma { @@ -92,21 +91,8 @@ namespace Barotrauma public class StoreInfo { - private int balance; - public Identifier Identifier { get; } - public int Balance - { - get - { - return balance; - } - set - { - balance = value; - ActiveBalanceStatus = Location.GetStoreBalanceStatus(value); - } - } + public int Balance { get; set; } public List Stock { get; } = new List(); public List DailySpecials { get; } = new List(); public List RequestedGoods { get; } = new List(); @@ -114,8 +100,6 @@ namespace Barotrauma /// In percentages. Larger values make buying more expensive and selling less profitable, and vice versa. /// public int PriceModifier { get; set; } - public StoreBalanceStatus ActiveBalanceStatus { get; private set; } - public Color BalanceColor => ActiveBalanceStatus.Color; public Location Location { get; } private StoreInfo(Location location) @@ -298,14 +282,7 @@ namespace Barotrauma price = Location.DailySpecialPriceModifier * price; } // Adjust by current location reputation - if (Location.Reputation.Value > 0.0f) - { - price = MathHelper.Lerp(1.0f, 1.0f - Location.StoreMaxReputationModifier, Location.Reputation.Value / Location.Reputation.MaxReputation) * price; - } - else - { - price = MathHelper.Lerp(1.0f, 1.0f + Location.StoreMaxReputationModifier, Location.Reputation.Value / Location.Reputation.MinReputation) * price; - } + price *= Location.GetStoreReputationModifier(true); // Price should never go below 1 mk return Math.Max((int)price, 1); } @@ -319,22 +296,13 @@ namespace Barotrauma float price = Location.StoreSellPriceModifier * priceInfo.Price; // Adjust by random price modifier price = (100 - PriceModifier) / 100.0f * price; - // Adjust by current store balance - price = ActiveBalanceStatus.SellPriceModifier * price; // Adjust by requested good status if (considerRequestedGoods && RequestedGoods.Contains(item)) { price = Location.RequestGoodPriceModifier * price; } // Adjust by current location reputation - if (Location.Reputation.Value > 0.0f) - { - price = MathHelper.Lerp(1.0f, 1.0f + Location.StoreMaxReputationModifier, Location.Reputation.Value / Location.Reputation.MaxReputation) * price; - } - else - { - price = MathHelper.Lerp(1.0f, 1.0f - Location.StoreMaxReputationModifier, Location.Reputation.Value / Location.Reputation.MinReputation) * price; - } + price *= Location.GetStoreReputationModifier(false); // Price should never go below 1 mk return Math.Max((int)price, 1); } @@ -353,7 +321,6 @@ namespace Barotrauma private float RequestGoodPriceModifier => Type.RequestGoodPriceModifier; public int StoreInitialBalance => Type.StoreInitialBalance; private int StorePriceModifierRange => Type.StorePriceModifierRange; - private List StoreBalanceStatuses => Type.StoreBalanceStatuses; /// /// How many map progress steps it takes before the discounts should be updated. @@ -1224,6 +1191,32 @@ namespace Barotrauma } } + public float GetStoreReputationModifier(bool buying) + { + if (buying) + { + if (Reputation.Value > 0.0f) + { + return MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation); + } + else + { + return MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation); + } + } + else + { + if (Reputation.Value > 0.0f) + { + return MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation); + } + else + { + return MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation); + } + } + } + public int GetExtraSpecialSalesCount() { var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); @@ -1231,21 +1224,6 @@ namespace Barotrauma return characters.Max(c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount)); } - public StoreBalanceStatus GetStoreBalanceStatus(int balance) - { - StoreBalanceStatus nextStatus = StoreBalanceStatuses[0]; - for (int i = 1; i < StoreBalanceStatuses.Count; i++) - { - var status = StoreBalanceStatuses[i]; - if (status.PercentageOfInitialBalance < nextStatus.PercentageOfInitialBalance && - ((float)balance / StoreInitialBalance) < status.PercentageOfInitialBalance) - { - nextStatus = status; - } - } - return nextStatus; - } - public void Discover(bool checkTalents = true) { if (Discovered) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index e81e2795d..efa64c5d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -88,27 +88,6 @@ namespace Barotrauma public int DailySpecialsCount { get; } = 1; public int RequestedGoodsCount { get; } = 1; - public List StoreBalanceStatuses { get; } = new List() - { - new StoreBalanceStatus(1.0f, 1.0f, Color.White), - new StoreBalanceStatus(0.5f, 0.75f, Color.Orange), - new StoreBalanceStatus(0.25f, 0.2f, Color.Red) - }; - - public struct StoreBalanceStatus - { - public float PercentageOfInitialBalance { get; } - public float SellPriceModifier { get; } - public Color Color { get; } - - public StoreBalanceStatus(float percentage, float sellPriceModifier, Color color) - { - PercentageOfInitialBalance = percentage; - SellPriceModifier = sellPriceModifier; - Color = color; - } - } - public override string ToString() { return $"LocationType (" + Identifier + ")"; @@ -208,18 +187,6 @@ namespace Barotrauma RequestGoodPriceModifier = subElement.GetAttributeFloat("requestgoodpricemodifier", RequestGoodPriceModifier); StoreInitialBalance = subElement.GetAttributeInt("initialbalance", StoreInitialBalance); StorePriceModifierRange = subElement.GetAttributeInt("pricemodifierrange", StorePriceModifierRange); - var balanceStatusElements = subElement.GetChildElements("balancestatus"); - if (balanceStatusElements.Any()) - { - StoreBalanceStatuses.Clear(); - foreach (var balanceStatusElement in balanceStatusElements) - { - float percentage = balanceStatusElement.GetAttributeFloat("percentage", 1.0f); - float modifier = balanceStatusElement.GetAttributeFloat("sellpricemodifier", 1.0f); - Color color = balanceStatusElement.GetAttributeColor("color", Color.White); - StoreBalanceStatuses.Add(new StoreBalanceStatus(percentage, modifier, color)); - } - } DailySpecialsCount = subElement.GetAttributeInt("dailyspecialscount", DailySpecialsCount); RequestedGoodsCount = subElement.GetAttributeInt("requestedgoodscount", RequestedGoodsCount); break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index 478350b10..192065b94 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -238,6 +238,16 @@ namespace Barotrauma } System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation."); + //ensure all paths from the starting location have 0 difficulty to make the 1st campaign round very easy + foreach (var locationConnection in StartLocation.Connections) + { + if (locationConnection.Difficulty > 0.0f) + { + locationConnection.Difficulty = 0.0f; + locationConnection.LevelData = new LevelData(locationConnection); + } + } + CurrentLocation.Discover(true); CurrentLocation.CreateStores(); @@ -509,25 +519,13 @@ namespace Barotrauma foreach (Location location in Locations) { - location.LevelData = new LevelData(location) - { - Difficulty = MathHelper.Clamp(location.MapPosition.X / Width * 100, 0.0f, 100.0f) - //Difficulty = MathHelper.Clamp(GetLevelDifficulty(location.MapPosition.X / Width), 0.0f, 100.0f) - }; + location.LevelData = new LevelData(location, MathHelper.Clamp(location.MapPosition.X / Width * 100, 0.0f, 100.0f)); location.UnlockInitialMissions(); } foreach (LocationConnection connection in Connections) { connection.LevelData = new LevelData(connection); } - - float GetLevelDifficulty(float areaDifficulty) - { - const float CurveModifier = 1.5f; - const float DifficultyMultiplier = 1.14f; - const float BaseDifficulty = -3f; - return (float)(1 - Math.Pow(1 - areaDifficulty, CurveModifier)) * DifficultyMultiplier * 100f + BaseDifficulty; - } } partial void GenerateLocationConnectionVisuals(); @@ -1015,8 +1013,7 @@ namespace Barotrauma { string prevName = location.Name; - var newType = LocationType.Prefabs[change.ChangeToType]; - if (newType == null) + 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."); return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index fb989ec73..e3b3d3d81 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -291,7 +291,7 @@ namespace Barotrauma } } - public virtual void Move(Vector2 amount) + public virtual void Move(Vector2 amount, bool ignoreContacts = false) { rect.X += (int)amount.X; rect.Y += (int)amount.Y; @@ -491,25 +491,33 @@ namespace Barotrauma protected void InsertToList() { - int i = 0; - if (Sprite == null) { mapEntityList.Add(this); return; } + int i = 0; while (i < mapEntityList.Count) { i++; - - Sprite existingSprite = mapEntityList[i - 1].Sprite; - if (existingSprite == null) continue; -#if CLIENT - if (existingSprite.Texture == this.Sprite.Texture) break; -#endif + if (mapEntityList[i - 1]?.Prefab == Prefab) + { + mapEntityList.Insert(i, this); + return; + } } +#if CLIENT + i = 0; + while (i < mapEntityList.Count) + { + i++; + Sprite existingSprite = mapEntityList[i - 1].Sprite; + if (existingSprite == null) { continue; } + if (existingSprite.Texture == this.Sprite.Texture) { break; } + } +#endif mapEntityList.Insert(i, this); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 295b92751..8e598475e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -1077,7 +1077,7 @@ namespace Barotrauma { foreach (Connection c in gapToRemove.ConnectedDoor.Item.Connections) { - c.Wires.ForEach(w => w?.Item.Remove()); + c.Wires.ToArray().ForEach(w => w?.Item.Remove()); } } @@ -1428,7 +1428,7 @@ namespace Barotrauma { foreach (Connection connection in linkedItem.Connections) { - foreach (Wire w in connection.Wires) + foreach (Wire w in connection.Wires.ToArray()) { w?.Item.Remove(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index d3901b682..b973831de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -354,7 +354,7 @@ namespace Barotrauma private set; } - public override void Move(Vector2 amount) + public override void Move(Vector2 amount, bool ignoreContacts = false) { if (!MathUtils.IsValid(amount)) { @@ -377,7 +377,15 @@ namespace Barotrauma Vector2 simAmount = ConvertUnits.ToSimUnits(amount); foreach (Body b in Bodies) { - b.SetTransform(b.Position + simAmount, b.Rotation); + Vector2 pos = b.Position + simAmount; + if (ignoreContacts) + { + b.SetTransformIgnoreContacts(ref pos, b.Rotation); + } + else + { + b.SetTransform(pos, b.Rotation); + } } } @@ -1208,7 +1216,7 @@ namespace Barotrauma private void UpdateSections() { - if (Bodies == null) return; + if (Bodies == null) { return; } foreach (Body b in Bodies) { GameMain.World.Remove(b); @@ -1281,9 +1289,9 @@ namespace Barotrauma Body newBody = GameMain.World.CreateRectangle( ConvertUnits.ToSimUnits(rect.Width), ConvertUnits.ToSimUnits(rect.Height), - 1.5f); - newBody.BodyType = BodyType.Static; - //newBody.Position = ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2.0f, rect.Y - rect.Height / 2.0f)); + 1.5f, + bodyType: BodyType.Static, + findNewContacts: false); newBody.Friction = 0.5f; newBody.OnCollision += OnWallCollision; newBody.CollisionCategories = (Prefab.Platform) ? Physics.CollisionPlatform : Physics.CollisionWall; @@ -1292,15 +1300,16 @@ namespace Barotrauma Vector2 structureCenter = ConvertUnits.ToSimUnits(Position); if (BodyRotation != 0.0f) { - newBody.Position = structureCenter + bodyOffset + new Vector2( + Vector2 pos = structureCenter + bodyOffset + new Vector2( (float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation), (float)Math.Sin(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation)) * ConvertUnits.ToSimUnits(diffFromCenter); - newBody.Rotation = -BodyRotation; + newBody.SetTransformIgnoreContacts(ref pos, -BodyRotation); } else { - newBody.Position = structureCenter + (IsHorizontal ? Vector2.UnitX : Vector2.UnitY) * ConvertUnits.ToSimUnits(diffFromCenter) + bodyOffset; + Vector2 pos = structureCenter + (IsHorizontal ? Vector2.UnitX : Vector2.UnitY) * ConvertUnits.ToSimUnits(diffFromCenter) + bodyOffset; + newBody.SetTransformIgnoreContacts(ref pos, newBody.Rotation); } if (createConvexHull) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index a046b1098..b5f7d87ba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -1302,189 +1302,198 @@ namespace Barotrauma public Submarine(SubmarineInfo info, bool showWarningMessages = true, Func> loadEntities = null, IdRemap linkedRemap = null) : base(null, Entity.NullEntityID) { Loading = true; - - loaded.Add(this); - - Info = new SubmarineInfo(info); - - ConnectedDockingPorts = new Dictionary(); - - //place the sub above the top of the level - HiddenSubPosition = HiddenSubStartPosition; - if (GameMain.GameSession != null && GameMain.GameSession.LevelData != null) + GameMain.World.Enabled = false; + try { - HiddenSubPosition += Vector2.UnitY * GameMain.GameSession.LevelData.Size.Y; - } + loaded.Add(this); - foreach (Submarine sub in loaded) - { - HiddenSubPosition += Vector2.UnitY * (sub.Borders.Height + 5000.0f); - } + Info = new SubmarineInfo(info); - IdOffset = IdRemap.DetermineNewOffset(); + ConnectedDockingPorts = new Dictionary(); - List newEntities = new List(); - if (loadEntities == null) - { - if (Info.SubmarineElement != null) + //place the sub above the top of the level + HiddenSubPosition = HiddenSubStartPosition; + if (GameMain.GameSession != null && GameMain.GameSession.LevelData != null) { - newEntities = MapEntity.LoadAll(this, Info.SubmarineElement, Info.FilePath, IdOffset); - } - } - else - { - newEntities = loadEntities(this); - newEntities.ForEach(me => me.Submarine = this); - } - - if (newEntities != null) - { - foreach (var e in newEntities) - { - if (linkedRemap != null) { e.ResolveLinks(linkedRemap); } - e.unresolvedLinkedToID = null; - } - } - - Vector2 center = Vector2.Zero; - var matchingHulls = Hull.HullList.FindAll(h => h.Submarine == this); - - if (matchingHulls.Any()) - { - Vector2 topLeft = new Vector2(matchingHulls[0].Rect.X, matchingHulls[0].Rect.Y); - Vector2 bottomRight = new Vector2(matchingHulls[0].Rect.X, matchingHulls[0].Rect.Y); - foreach (Hull hull in matchingHulls) - { - if (hull.Rect.X < topLeft.X) topLeft.X = hull.Rect.X; - if (hull.Rect.Y > topLeft.Y) topLeft.Y = hull.Rect.Y; - - if (hull.Rect.Right > bottomRight.X) bottomRight.X = hull.Rect.Right; - if (hull.Rect.Y - hull.Rect.Height < bottomRight.Y) bottomRight.Y = hull.Rect.Y - hull.Rect.Height; + HiddenSubPosition += Vector2.UnitY * GameMain.GameSession.LevelData.Size.Y; } - center = (topLeft + bottomRight) / 2.0f; - center.X -= center.X % GridSize.X; - center.Y -= center.Y % GridSize.Y; - - RepositionEntities(-center, MapEntity.mapEntityList.Where(me => me.Submarine == this)); - - subBody = new SubmarineBody(this, showWarningMessages); - subBody.SetPosition(HiddenSubPosition); - - if (info.IsOutpost) + foreach (Submarine sub in loaded) { - ShowSonarMarker = false; - PhysicsBody.FarseerBody.BodyType = BodyType.Static; - TeamID = CharacterTeamType.FriendlyNPC; + HiddenSubPosition += Vector2.UnitY * (sub.Borders.Height + 5000.0f); + } - bool indestructible = - GameMain.NetworkMember != null && - !GameMain.NetworkMember.ServerSettings.DestructibleOutposts && - !(info.OutpostGenerationParams?.AlwaysDestructible ?? false); + IdOffset = IdRemap.DetermineNewOffset(); - foreach (MapEntity me in MapEntity.mapEntityList) + List newEntities = new List(); + if (loadEntities == null) + { + if (Info.SubmarineElement != null) { - if (me.Submarine != this) { continue; } - if (me is Item item) - { - item.SpawnedInCurrentOutpost = info.OutpostGenerationParams != null; - item.AllowStealing = info.OutpostGenerationParams?.AllowStealing ?? true; - if (item.GetComponent() != null && indestructible) - { - item.Indestructible = true; - } - foreach (ItemComponent ic in item.Components) - { - if (ic is ConnectionPanel connectionPanel) - { - //prevent rewiring - if (info.OutpostGenerationParams != null && !info.OutpostGenerationParams.AlwaysRewireable) - { - connectionPanel.Locked = true; - } - } - else if (ic is Holdable holdable && holdable.Attached && item.GetComponent() == null) - { - //prevent deattaching items from walls -#if CLIENT - if (GameMain.GameSession?.GameMode is TutorialMode) { continue; } -#endif - holdable.CanBePicked = false; - holdable.CanBeSelected = false; - } - } - } - else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts && indestructible) - { - structure.Indestructible = true; - } + newEntities = MapEntity.LoadAll(this, Info.SubmarineElement, Info.FilePath, IdOffset); } } - else if (info.IsRuin) + else { - ShowSonarMarker = false; - PhysicsBody.FarseerBody.BodyType = BodyType.Static; + newEntities = loadEntities(this); + newEntities.ForEach(me => me.Submarine = this); } - } - if (entityGrid != null) - { - Hull.EntityGrids.Remove(entityGrid); - entityGrid = null; - } - entityGrid = Hull.GenerateEntityGrid(this); - - for (int i = 0; i < MapEntity.mapEntityList.Count; i++) - { - if (MapEntity.mapEntityList[i].Submarine != this) { continue; } - MapEntity.mapEntityList[i].Move(HiddenSubPosition); - } - - Loading = false; - - MapEntity.MapLoaded(newEntities, true); - foreach (MapEntity me in MapEntity.mapEntityList) - { - if (me is LinkedSubmarine linkedSub && linkedSub.Submarine == this) + if (newEntities != null) { - linkedSub.LinkDummyToMainSubmarine(); + foreach (var e in newEntities) + { + if (linkedRemap != null) { e.ResolveLinks(linkedRemap); } + e.unresolvedLinkedToID = null; + } } - } - foreach (Hull hull in matchingHulls) - { - if (string.IsNullOrEmpty(hull.RoomName))// || !hull.RoomName.Contains("roomname.", StringComparison.OrdinalIgnoreCase)) + Vector2 center = Vector2.Zero; + var matchingHulls = Hull.HullList.FindAll(h => h.Submarine == this); + + if (matchingHulls.Any()) { - hull.RoomName = hull.CreateRoomName(); - } - } + Vector2 topLeft = new Vector2(matchingHulls[0].Rect.X, matchingHulls[0].Rect.Y); + Vector2 bottomRight = new Vector2(matchingHulls[0].Rect.X, matchingHulls[0].Rect.Y); + foreach (Hull hull in matchingHulls) + { + if (hull.Rect.X < topLeft.X) topLeft.X = hull.Rect.X; + if (hull.Rect.Y > topLeft.Y) topLeft.Y = hull.Rect.Y; - if (GameMain.GameSession?.Campaign?.UpgradeManager != null) - { - GameMain.GameSession.Campaign.UpgradeManager.OnUpgradesChanged += ResetCrushDepth; - } + if (hull.Rect.Right > bottomRight.X) bottomRight.X = hull.Rect.Right; + if (hull.Rect.Y - hull.Rect.Height < bottomRight.Y) bottomRight.Y = hull.Rect.Y - hull.Rect.Height; + } + + center = (topLeft + bottomRight) / 2.0f; + center.X -= center.X % GridSize.X; + center.Y -= center.Y % GridSize.Y; + + RepositionEntities(-center, MapEntity.mapEntityList.Where(me => me.Submarine == this)); + + subBody = new SubmarineBody(this, showWarningMessages); + Vector2 pos = ConvertUnits.ToSimUnits(HiddenSubPosition); + subBody.Body.FarseerBody.SetTransformIgnoreContacts(ref pos, 0.0f); + + if (info.IsOutpost) + { + ShowSonarMarker = false; + PhysicsBody.FarseerBody.BodyType = BodyType.Static; + TeamID = CharacterTeamType.FriendlyNPC; + + bool indestructible = + GameMain.NetworkMember != null && + !GameMain.NetworkMember.ServerSettings.DestructibleOutposts && + !(info.OutpostGenerationParams?.AlwaysDestructible ?? false); + + foreach (MapEntity me in MapEntity.mapEntityList) + { + if (me.Submarine != this) { continue; } + if (me is Item item) + { + item.SpawnedInCurrentOutpost = info.OutpostGenerationParams != null; + item.AllowStealing = info.OutpostGenerationParams?.AllowStealing ?? true; + if (item.GetComponent() != null && indestructible) + { + item.Indestructible = true; + } + foreach (ItemComponent ic in item.Components) + { + if (ic is ConnectionPanel connectionPanel) + { + //prevent rewiring + if (info.OutpostGenerationParams != null && !info.OutpostGenerationParams.AlwaysRewireable) + { + connectionPanel.Locked = true; + } + } + else if (ic is Holdable holdable && holdable.Attached && item.GetComponent() == null) + { + //prevent deattaching items from walls +#if CLIENT + if (GameMain.GameSession?.GameMode is TutorialMode) { continue; } +#endif + holdable.CanBePicked = false; + holdable.CanBeSelected = false; + } + } + } + else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts && indestructible) + { + structure.Indestructible = true; + } + } + } + else if (info.IsRuin) + { + ShowSonarMarker = false; + PhysicsBody.FarseerBody.BodyType = BodyType.Static; + } + } + + if (entityGrid != null) + { + Hull.EntityGrids.Remove(entityGrid); + entityGrid = null; + } + entityGrid = Hull.GenerateEntityGrid(this); + + for (int i = 0; i < MapEntity.mapEntityList.Count; i++) + { + if (MapEntity.mapEntityList[i].Submarine != this) { continue; } + MapEntity.mapEntityList[i].Move(HiddenSubPosition, ignoreContacts: true); + } + + Loading = false; + + MapEntity.MapLoaded(newEntities, true); + foreach (MapEntity me in MapEntity.mapEntityList) + { + if (me is LinkedSubmarine linkedSub && linkedSub.Submarine == this) + { + linkedSub.LinkDummyToMainSubmarine(); + } + } + + foreach (Hull hull in matchingHulls) + { + if (string.IsNullOrEmpty(hull.RoomName))// || !hull.RoomName.Contains("roomname.", StringComparison.OrdinalIgnoreCase)) + { + hull.RoomName = hull.CreateRoomName(); + } + } + + if (GameMain.GameSession?.Campaign?.UpgradeManager != null) + { + GameMain.GameSession.Campaign.UpgradeManager.OnUpgradesChanged += ResetCrushDepth; + } #if CLIENT - GameMain.LightManager.OnMapLoaded(); + GameMain.LightManager.OnMapLoaded(); #endif - //if the sub was made using an older version, - //halve the brightness of the lights to make them look (almost) right on the new lighting formula - if (showWarningMessages && - !string.IsNullOrEmpty(Info.FilePath) && - Screen.Selected != GameMain.SubEditorScreen && - (Info.GameVersion == null || Info.GameVersion < new Version("0.8.9.0"))) - { - DebugConsole.ThrowError("The submarine \"" + Info.Name + "\" was made using an older version of the Barotrauma that used a different formula to calculate the lighting. " - + "The game automatically adjusts the lights make them look better with the new formula, but it's recommended to open the submarine in the submarine editor and make sure everything looks right after the automatic conversion."); - foreach (Item item in Item.ItemList) + //if the sub was made using an older version, + //halve the brightness of the lights to make them look (almost) right on the new lighting formula + if (showWarningMessages && + !string.IsNullOrEmpty(Info.FilePath) && + Screen.Selected != GameMain.SubEditorScreen && + (Info.GameVersion == null || Info.GameVersion < new Version("0.8.9.0"))) { - if (item.Submarine != this) continue; - if (item.ParentInventory != null || item.body != null) continue; - var lightComponent = item.GetComponent(); - if (lightComponent != null) lightComponent.LightColor = new Color(lightComponent.LightColor, lightComponent.LightColor.A / 255.0f * 0.5f); + DebugConsole.ThrowError("The submarine \"" + Info.Name + "\" was made using an older version of the Barotrauma that used a different formula to calculate the lighting. " + + "The game automatically adjusts the lights make them look better with the new formula, but it's recommended to open the submarine in the submarine editor and make sure everything looks right after the automatic conversion."); + foreach (Item item in Item.ItemList) + { + if (item.Submarine != this) continue; + if (item.ParentInventory != null || item.body != null) continue; + var lightComponent = item.GetComponent(); + if (lightComponent != null) lightComponent.LightColor = new Color(lightComponent.LightColor, lightComponent.LightColor.A / 255.0f * 0.5f); + } } + GenerateOutdoorNodes(); + } + finally + { + Loading = false; + GameMain.World.Enabled = true; } - GenerateOutdoorNodes(); } protected override ushort DetermineID(ushort id, Submarine submarine) @@ -1495,10 +1504,7 @@ namespace Barotrauma public static Submarine Load(SubmarineInfo info, bool unloadPrevious, IdRemap linkedRemap = null) { if (unloadPrevious) { Unload(); } - - Submarine sub = new Submarine(info, false, linkedRemap: linkedRemap); - - return sub; + return new Submarine(info, false, linkedRemap: linkedRemap); } private void ResetCrushDepth() @@ -1599,18 +1605,14 @@ namespace Barotrauma { if (item.FindParentInventory(inv => inv is CharacterInventory) != null) { continue; } #if CLIENT - if (Screen.Selected != GameMain.SubEditorScreen) - { - if (e.Submarine != this && item.GetRootContainer()?.Submarine != this) { continue; } - } - else + if (Screen.Selected == GameMain.SubEditorScreen) { e.Submarine = this; } -#else - if (e.Submarine != this && item.GetRootContainer()?.Submarine != this) { continue; } #endif - + if (e.Submarine != this) { continue; } + var rootContainer = item.GetRootContainer(); + if (rootContainer != null && rootContainer.Submarine != this) { continue; } } else { @@ -1851,5 +1853,32 @@ namespace Barotrauma } public void RefreshOutdoorNodes() => OutdoorNodes.ForEach(n => n?.Waypoint?.FindHull()); + + public Item FindContainerFor(Item item, bool onlyPrimary, bool checkTransferConditions = false) + { + var potentialContainers = new List(); + foreach (Item potentialContainer in Item.ItemList) + { + if (potentialContainer.Removed) { continue; } + if (potentialContainer.NonInteractable) { continue; } + if (potentialContainer.HiddenInGame) { continue; } + if (potentialContainer.Submarine != this) { continue; } + if (potentialContainer == item) { continue; } + if (potentialContainer.Condition <= 0) { continue; } + if (potentialContainer.OwnInventory == null) { continue; } + if (potentialContainer.GetRootInventoryOwner() != potentialContainer) { continue; } + var container = potentialContainer.GetComponent(); + if (container == null) { continue; } + if (!potentialContainer.OwnInventory.CanBePut(item)) { continue; } + if (!container.ShouldBeContained(item, out _)) { continue; } + if (!item.Prefab.IsContainerPreferred(item, container, out bool isPreferencesDefined, out bool isSecondary, checkTransferConditions: checkTransferConditions) || !isPreferencesDefined || onlyPrimary && isSecondary) { continue; } + potentialContainers.Add(potentialContainer); + if (!isSecondary) + { + break; + } + } + return potentialContainers.LastOrDefault(); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 3216589a1..c2340805b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -136,7 +136,17 @@ namespace Barotrauma HullVertices = convexHull; - farseerBody = GameMain.World.CreateBody(); + farseerBody = GameMain.World.CreateBody(findNewContacts: false, bodyType: BodyType.Dynamic); + var collisionCategory = Physics.CollisionWall; + var collidesWith = + Physics.CollisionItem | + Physics.CollisionLevel | + Physics.CollisionCharacter | + Physics.CollisionProjectile | + Physics.CollisionWall; + farseerBody.CollisionCategories = collisionCategory; + farseerBody.CollidesWith = collidesWith; + farseerBody.Enabled = false; farseerBody.UserData = this; foreach (var mapEntity in MapEntity.mapEntityList) { @@ -152,7 +162,9 @@ namespace Barotrauma ConvertUnits.ToSimUnits(wall.BodyHeight), 50.0f, -wall.BodyRotation, - ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + wall.BodyOffset)).UserData = wall; + ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + wall.BodyOffset), + collisionCategory, + collidesWith).UserData = wall; } } @@ -167,7 +179,9 @@ namespace Barotrauma ConvertUnits.ToSimUnits(rect.Width), ConvertUnits.ToSimUnits(rect.Height), 100.0f, - ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2))).UserData = hull; + ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2)), + collisionCategory, + collidesWith).UserData = hull; } foreach (Item item in Item.ItemList) @@ -191,47 +205,40 @@ namespace Barotrauma if (width > 0.0f && height > 0.0f) { - item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simHeight, 5.0f, simPos)); + 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); } else if (radius > 0.0f && width > 0.0f) { - item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simRadius * 2, 5.0f, simPos)); - item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitX * simWidth / 2)); - item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simWidth / 2)); + 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); } else if (radius > 0.0f && height > 0.0f) { - item.StaticFixtures.Add(farseerBody.CreateRectangle(simRadius * 2, height, 5.0f, simPos)); - item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitY * simHeight / 2)); - item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simHeight / 2)); + 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); } else if (radius > 0.0f) { - item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos)); + 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); } + item.StaticFixtures.ForEach(f => f.UserData = item); } Borders = new Rectangle((int)minExtents.X, (int)maxExtents.Y, (int)(maxExtents.X - minExtents.X), (int)(maxExtents.Y - minExtents.Y)); VisibleBorders = new Rectangle((int)visibleMinExtents.X, (int)visibleMaxExtents.Y, (int)(visibleMaxExtents.X - visibleMinExtents.X), (int)(visibleMaxExtents.Y - visibleMinExtents.Y)); } - farseerBody.BodyType = BodyType.Dynamic; - farseerBody.CollisionCategories = Physics.CollisionWall; - farseerBody.CollidesWith = - Physics.CollisionItem | - Physics.CollisionLevel | - Physics.CollisionCharacter | - Physics.CollisionProjectile | - Physics.CollisionWall; - + farseerBody.Enabled = true; farseerBody.Restitution = Restitution; farseerBody.Friction = Friction; farseerBody.FixedRotation = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs index f48d81a46..b269b2c76 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs @@ -107,10 +107,25 @@ namespace Barotrauma.Networking return -1; } + //FIXME workaround for crash when closing the server under .NET 6.0, not sure if this is the proper way to fix it but it prevents it from crashing the client. - Markus +#if NET6_0 + try + { + if (readTask.IsCompleted || readTask.Wait(100, readCancellationToken.Token)) + { + break; + } + } + catch (OperationCanceledException) + { + return -1; + } +#else if (readTask.IsCompleted || readTask.Wait(timeOut)) { break; } +#endif } if (readTask.Status != TaskStatus.RanToCompletion) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index 50a94dbe5..b80fd2455 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -21,6 +21,8 @@ namespace Barotrauma.Networking private readonly NetworkMember networkMember; private readonly Steering shuttleSteering; private readonly List shuttleDoors; + private const string RespawnContainerTag = "respawncontainer"; + private readonly ItemContainer respawnContainer; //items created during respawn //any respawn items left in the shuttle are removed when the shuttle despawns @@ -100,13 +102,18 @@ namespace Barotrauma.Networking shuttleDoors = new List(); foreach (Item item in Item.ItemList) { - if (item.Submarine != RespawnShuttle) continue; + if (item.Submarine != RespawnShuttle) { continue; } + + if (item.HasTag(RespawnContainerTag)) + { + respawnContainer = item.GetComponent(); + } var steering = item.GetComponent(); - if (steering != null) shuttleSteering = steering; + if (steering != null) { shuttleSteering = steering; } var door = item.GetComponent(); - if (door != null) shuttleDoors.Add(door); + if (door != null) { shuttleDoors.Add(door); } //lock all wires to prevent the players from messing up the electronics var connectionPanel = item.GetComponent(); @@ -227,14 +234,14 @@ namespace Barotrauma.Networking despawnTime = ReturnTime + new TimeSpan(0, 0, seconds: 30); #endif - if (RespawnShuttle == null) return; + if (RespawnShuttle == null) { return; } foreach (Item item in Item.ItemList) { if (item.Submarine != RespawnShuttle) { continue; } //remove respawn items that have been left in the shuttle - if (respawnItems.Contains(item)) + if (respawnItems.Contains(item) || respawnContainer?.Item != null && item.IsOwnedBy(respawnContainer.Item)) { Spawner.AddItemToRemoveQueue(item); continue; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 736099f64..10e45816a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -39,6 +39,12 @@ namespace Barotrauma.Networking SomethingDifferent = 4 } + internal enum LootedMoneyDestination + { + Bank, + Wallet + } + partial class ServerSettings : ISerializableEntity { public const string SettingsFile = "serversettings.xml"; @@ -901,10 +907,16 @@ namespace Barotrauma.Networking set; } + [Serialize(LootedMoneyDestination.Bank, IsPropertySaveable.Yes)] + public LootedMoneyDestination LootedMoneyDestination { get; set; } + + [Serialize(999999, IsPropertySaveable.Yes)] + public int MaximumTransferRequest { get; set; } + private int maxMissionCount = CampaignSettings.DefaultMaxMissionCount; [Serialize(CampaignSettings.DefaultMaxMissionCount, IsPropertySaveable.Yes)] - public int MaxMissionCount + public int MaxMissionCount { get { return maxMissionCount; } set { maxMissionCount = MathHelper.Clamp(value, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index ae1f8adf7..74da0ce7d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -344,14 +344,14 @@ namespace Barotrauma } } - public PhysicsBody(XElement element, float scale = 1.0f) : this(element, Vector2.Zero, scale) { } - public PhysicsBody(ColliderParams cParams) : this(cParams, Vector2.Zero) { } - public PhysicsBody(LimbParams lParams) : this(lParams, Vector2.Zero) { } + public PhysicsBody(XElement element, float scale = 1.0f, bool findNewContacts = true) : this(element, Vector2.Zero, scale, findNewContacts: findNewContacts) { } + public PhysicsBody(ColliderParams cParams, bool findNewContacts = true) : this(cParams, Vector2.Zero, findNewContacts) { } + public PhysicsBody(LimbParams lParams, bool findNewContacts = true) : this(lParams, Vector2.Zero, findNewContacts) { } - public PhysicsBody(float width, float height, float radius, float density) + public PhysicsBody(float width, float height, float radius, float density, BodyType bodyType, Category collisionCategory, Category collidesWith, bool findNewContacts = true) { density = Math.Max(density, MinDensity); - CreateBody(width, height, radius, density); + CreateBody(width, height, radius, density, bodyType, collisionCategory, collidesWith, findNewContacts); LastSentPosition = FarseerBody.Position; list.Add(this); } @@ -359,21 +359,21 @@ namespace Barotrauma public PhysicsBody(Body farseerBody) { FarseerBody = farseerBody; - if (FarseerBody.UserData == null) FarseerBody.UserData = this; + if (FarseerBody.UserData == null) { FarseerBody.UserData = this; } LastSentPosition = FarseerBody.Position; list.Add(this); } - public PhysicsBody(ColliderParams colliderParams, Vector2 position) + public PhysicsBody(ColliderParams colliderParams, Vector2 position, bool findNewContacts = true) { float radius = ConvertUnits.ToSimUnits(colliderParams.Radius) * colliderParams.Ragdoll.LimbScale; float height = ConvertUnits.ToSimUnits(colliderParams.Height) * colliderParams.Ragdoll.LimbScale; float width = ConvertUnits.ToSimUnits(colliderParams.Width) * colliderParams.Ragdoll.LimbScale; density = 10; - CreateBody(width, height, radius, density); - FarseerBody.BodyType = BodyType.Dynamic; - FarseerBody.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel; - FarseerBody.CollisionCategories = Physics.CollisionCharacter; + CreateBody(width, height, radius, density, BodyType.Dynamic, + Physics.CollisionCharacter, + Physics.CollisionWall | Physics.CollisionLevel, + findNewContacts); FarseerBody.AngularDamping = DefaultAngularDamping; FarseerBody.FixedRotation = true; FarseerBody.Friction = 0.05f; @@ -383,16 +383,24 @@ namespace Barotrauma list.Add(this); } - public PhysicsBody(LimbParams limbParams, Vector2 position) + public PhysicsBody(LimbParams limbParams, Vector2 position, bool findNewContacts = true) { float radius = ConvertUnits.ToSimUnits(limbParams.Radius) * limbParams.Scale * limbParams.Ragdoll.LimbScale; float height = ConvertUnits.ToSimUnits(limbParams.Height) * limbParams.Scale * limbParams.Ragdoll.LimbScale; float width = ConvertUnits.ToSimUnits(limbParams.Width) * limbParams.Scale * limbParams.Ragdoll.LimbScale; density = Math.Max(limbParams.Density, MinDensity); - CreateBody(width, height, radius, density); - FarseerBody.BodyType = BodyType.Dynamic; - FarseerBody.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel; - FarseerBody.CollisionCategories = Physics.CollisionItem; + + Category collisionCategory = Physics.CollisionCharacter; + Category collidesWith = Physics.CollisionAll & ~Physics.CollisionCharacter & ~Physics.CollisionItem & ~Physics.CollisionItemBlocking; + if (limbParams.IgnoreCollisions) + { + collisionCategory = Category.None; + collidesWith = Category.None; + } + CreateBody(width, height, radius, density, BodyType.Dynamic, + collisionCategory: collisionCategory, + collidesWith: collidesWith, + findNewContacts: findNewContacts); FarseerBody.Friction = limbParams.Friction; FarseerBody.Restitution = limbParams.Restitution; FarseerBody.AngularDamping = limbParams.AngularDamping; @@ -402,17 +410,14 @@ namespace Barotrauma list.Add(this); } - public PhysicsBody(XElement element, Vector2 position, float scale = 1.0f, float? forceDensity = null) + public PhysicsBody(XElement element, Vector2 position, float scale = 1.0f, float? forceDensity = null, Category collisionCategory = Physics.CollisionItem, Category collidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform, bool findNewContacts = true) { float radius = ConvertUnits.ToSimUnits(element.GetAttributeFloat("radius", 0.0f)) * scale; float height = ConvertUnits.ToSimUnits(element.GetAttributeFloat("height", 0.0f)) * scale; float width = ConvertUnits.ToSimUnits(element.GetAttributeFloat("width", 0.0f)) * scale; density = Math.Max(forceDensity ?? element.GetAttributeFloat("density", 10.0f), MinDensity); - CreateBody(width, height, radius, density); Enum.TryParse(element.GetAttributeString("bodytype", "Dynamic"), out BodyType bodyType); - FarseerBody.BodyType = bodyType; - FarseerBody.CollisionCategories = Physics.CollisionItem; - FarseerBody.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform; + CreateBody(width, height, radius, density, bodyType, collisionCategory, collidesWith, findNewContacts); FarseerBody.Friction = element.GetAttributeFloat("friction", 0.5f); FarseerBody.Restitution = element.GetAttributeFloat("restitution", 0.05f); FarseerBody.UserData = this; @@ -421,7 +426,7 @@ namespace Barotrauma list.Add(this); } - private void CreateBody(float width, float height, float radius, float density) + private void CreateBody(float width, float height, float radius, float density, BodyType bodyType, Category collisionCategory, Category collidesWith, bool findNewContacts = true) { if (IsValidShape(radius, height, width)) { @@ -429,16 +434,16 @@ namespace Barotrauma switch (bodyShape) { case Shape.Capsule: - FarseerBody = GameMain.World.CreateCapsule(height, radius, density); + FarseerBody = GameMain.World.CreateCapsule(height, radius, density, bodyType: bodyType, collisionCategory: collisionCategory, collidesWith: collidesWith, findNewContacts: findNewContacts); ; break; case Shape.HorizontalCapsule: - FarseerBody = GameMain.World.CreateCapsuleHorizontal(width, radius, density); + FarseerBody = GameMain.World.CreateCapsuleHorizontal(width, radius, density, bodyType: bodyType, collisionCategory: collisionCategory, collidesWith: collidesWith, findNewContacts: findNewContacts); break; case Shape.Circle: - FarseerBody = GameMain.World.CreateCircle(radius, density); + FarseerBody = GameMain.World.CreateCircle(radius, density, bodyType: bodyType, collisionCategory: collisionCategory, collidesWith: collidesWith, findNewContacts: findNewContacts); break; case Shape.Rectangle: - FarseerBody = GameMain.World.CreateRectangle(width, height, density); + FarseerBody = GameMain.World.CreateRectangle(width, height, density, bodyType: bodyType, collisionCategory: collisionCategory, collidesWith: collidesWith, findNewContacts: findNewContacts); break; default: throw new NotImplementedException(bodyShape.ToString()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/Prefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/Prefab.cs index 1b40954db..c86e6050c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/Prefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/Prefab.cs @@ -20,9 +20,9 @@ namespace Barotrauma { if (!potentialCallFromConstructor) { return; } StackTrace st = new StackTrace(skipFrames: 2, fNeedFileInfo: false); - for (int i = st.FrameCount-1; i >= 0; i--) + for (int i = st.FrameCount - 1; i >= 0; i--) { - if (st.GetFrame(i)?.GetMethod() is {IsConstructor: true, DeclaringType: { } declaringType} + if (st.GetFrame(i)?.GetMethod() is { IsConstructor: true, DeclaringType: { } declaringType } && Types.Contains(declaringType)) { throw new Exception("Called disallowed method from within a prefab's constructor!"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index d1aac1552..5c0e19cfd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -301,7 +301,6 @@ namespace Barotrauma { InputType.Down, Keys.S }, { InputType.Left, Keys.A }, { InputType.Right, Keys.D }, - { InputType.ToggleInventory, Keys.Q }, { InputType.SelectNextCharacter, Keys.Z }, { InputType.SelectPreviousCharacter, Keys.X }, @@ -452,19 +451,19 @@ namespace Barotrauma public static void SetCurrentConfig(in Config newConfig) { - bool setGraphicsMode = - currentConfig.Graphics.Width != newConfig.Graphics.Width - || currentConfig.Graphics.Height != newConfig.Graphics.Height - || currentConfig.Graphics.VSync != newConfig.Graphics.VSync - || currentConfig.Graphics.DisplayMode != newConfig.Graphics.DisplayMode; - + bool resolutionChanged = + currentConfig.Graphics.Width != newConfig.Graphics.Width || + currentConfig.Graphics.Height != newConfig.Graphics.Height; bool languageChanged = currentConfig.Language != newConfig.Language; - bool audioOutputChanged = currentConfig.Audio.AudioOutputDevice != newConfig.Audio.AudioOutputDevice; bool voiceCaptureChanged = currentConfig.Audio.VoiceCaptureDevice != newConfig.Audio.VoiceCaptureDevice; - bool textScaleChanged = Math.Abs(currentConfig.Graphics.TextScale - newConfig.Graphics.TextScale) > MathF.Pow(2.0f, -7); + bool setGraphicsMode = + resolutionChanged || + currentConfig.Graphics.VSync != newConfig.Graphics.VSync || + currentConfig.Graphics.DisplayMode != newConfig.Graphics.DisplayMode; + currentConfig = newConfig; #if CLIENT @@ -483,7 +482,7 @@ namespace Barotrauma VoipCapture.ChangeCaptureDevice(currentConfig.Audio.VoiceCaptureDevice); } - if (textScaleChanged) + if (textScaleChanged || resolutionChanged) { foreach (var font in GUIStyle.Fonts.Values) { @@ -504,23 +503,11 @@ namespace Barotrauma XElement graphicsElement = new XElement("graphicssettings"); root.Add(graphicsElement); currentConfig.Graphics.SerializeElement(graphicsElement); - -#region Backwards compatibility crap -#warning TODO: remove once modding refactor ships in a stable release - XElement backwardsCompatibilityGraphicsMode = new XElement(graphicsElement); root.Add(backwardsCompatibilityGraphicsMode); - backwardsCompatibilityGraphicsMode.Name = "graphicsmode"; -#endregion - + XElement audioElement = new XElement("audio"); root.Add(audioElement); currentConfig.Audio.SerializeElement(audioElement); XElement contentPackagesElement = new XElement("contentpackages"); root.Add(contentPackagesElement); -#region More backwards compatibility crap - XComment backwardsCompatibleComment = new XComment("Backwards compatibility"); contentPackagesElement.Add(backwardsCompatibleComment); -#warning TODO: remove once modding refactor ships in a stable release - XElement backwardsCompatibleCoreElement = new XElement("core"); contentPackagesElement.Add(backwardsCompatibleCoreElement); - backwardsCompatibleCoreElement.SetAttributeValue("name", "Vanilla 0.9"); -#endregion XComment corePackageComment = new XComment(ContentPackageManager.EnabledPackages.Core?.Name ?? "Vanilla"); contentPackagesElement.Add(corePackageComment); XElement corePackageElement = new XElement(ContentPackageManager.CorePackageElementName); contentPackagesElement.Add(corePackageElement); corePackageElement.SetAttributeValue("path", ContentPackageManager.EnabledPackages.Core?.Path ?? ContentPackageManager.VanillaFileList); diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index 45aa60f26..f86226e55 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -1,8 +1,7 @@ using System; -using System.Collections.Generic; using System.Globalization; -using System.Xml.Linq; using System.Linq; +using System.Xml.Linq; namespace Barotrauma { diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 017a10301..fd364e4f8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -320,7 +320,7 @@ namespace Barotrauma private readonly int useItemCount; - private readonly bool removeItem, removeCharacter, breakLimb, hideLimb; + private readonly bool removeItem, dropContainedItems, removeCharacter, breakLimb, hideLimb; private readonly float hideLimbTimer; public readonly ActionType type = ActionType.OnActive; @@ -608,6 +608,9 @@ namespace Barotrauma case "removeitem": removeItem = true; break; + case "dropcontaineditems": + dropContainedItems = true; + break; case "removecharacter": removeCharacter = true; break; @@ -1224,6 +1227,22 @@ namespace Barotrauma } } + if (dropContainedItems) + { + for (int i = 0; i < targets.Count; i++) + { + if (targets[i] is Item item) + { + foreach (var itemContainer in item.GetComponents()) + { + foreach (var containedItem in itemContainer.Inventory.AllItemsMod) + { + containedItem.Drop(dropper: null); + } + } + } + } + } if (removeItem) { for (int i = 0; i < targets.Count; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index 09e9c36ce..46a58055b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -324,7 +324,7 @@ namespace Barotrauma.Steam using (var copyIndicator = new CopyIndicator(copyIndicatorPath)) { - await CopyDirectory(itemDirectory, modPathDirName ?? modName, itemDirectory, installDir); + await CopyDirectory(itemDirectory, modPathDirName ?? modName, itemDirectory, installDir, ShouldCorrectPaths.Yes); string fileListDestPath = Path.Combine(installDir, ContentPackage.FileListFileName); XDocument fileListDest = XMLExtensions.TryLoadXml(fileListDestPath); @@ -358,12 +358,15 @@ namespace Barotrauma.Steam string val = attribute.Value.CleanUpPathCrossPlatform(correctFilenameCase: false); + bool isPath = false; + //Handle mods that have been mangled by pre-modding-refactor //copying of post-modding-refactor mods (what a clusterfuck) int modDirStrIndex = val.IndexOf(ContentPath.ModDirStr, StringComparison.OrdinalIgnoreCase); if (modDirStrIndex >= 0) { val = val[modDirStrIndex..]; + isPath = true; } //Handle really old mods (0.9.0.4-era) that might be structured as @@ -372,6 +375,7 @@ namespace Barotrauma.Steam if (File.Exists(fullSrcPath)) { val = $"{ContentPath.ModDirStr}/{val}"; + isPath = true; } //Handle old mods that installed to the fixed Mods directory @@ -380,6 +384,7 @@ namespace Barotrauma.Steam if (val.StartsWith(oldModDir, StringComparison.OrdinalIgnoreCase)) { val = $"{ContentPath.ModDirStr}{val.Remove(0, oldModDir.Length)}"; + isPath = true; } //Handle old mods that depend on other mods else if (val.StartsWith("Mods/", StringComparison.OrdinalIgnoreCase)) @@ -387,13 +392,15 @@ namespace Barotrauma.Steam string otherModName = val.Substring(val.IndexOf('/')+1); otherModName = otherModName.Substring(0, otherModName.IndexOf('/')); val = $"{string.Format(ContentPath.OtherModDirFmt, otherModName)}{val.Remove(0, $"Mods/{otherModName}".Length)}"; + isPath = true; } //Handle really old mods that installed Submarines in the wrong place else if (val.StartsWith("Submarines/", StringComparison.OrdinalIgnoreCase)) { val = $"{ContentPath.ModDirStr}/{val}"; + isPath = true; } - attribute.Value = val; + if (isPath) { attribute.Value = val; } } await Task.WhenAll( element.Elements() @@ -403,11 +410,11 @@ namespace Barotrauma.Steam element: subElement))); } - private static async Task CopyFile(string fileListDir, string modName, string from, string to) + private static async Task CopyFile(string fileListDir, string modName, string from, string to, ShouldCorrectPaths shouldCorrectPaths) { await Task.Yield(); Identifier extension = Path.GetExtension(from).ToIdentifier(); - if (extension == ".xml") + if (extension == ".xml" && shouldCorrectPaths == ShouldCorrectPaths.Yes) { try { @@ -436,7 +443,12 @@ namespace Barotrauma.Steam File.Copy(from, to, overwrite: true); } - public static async Task CopyDirectory(string fileListDir, string modName, string from, string to) + public enum ShouldCorrectPaths + { + Yes, No + } + + public static async Task CopyDirectory(string fileListDir, string modName, string from, string to, ShouldCorrectPaths shouldCorrectPaths) { from = Path.GetFullPath(from); to = Path.GetFullPath(to); Directory.CreateDirectory(to); @@ -448,10 +460,10 @@ namespace Barotrauma.Steam string[] subDirs = Directory.GetDirectories(from); foreach (var file in files) { - await CopyFile(fileListDir, modName, file, convertFromTo(file)); + await CopyFile(fileListDir, modName, file, convertFromTo(file), shouldCorrectPaths); } - foreach (var dir in subDirs) { await CopyDirectory(fileListDir, modName, dir, convertFromTo(dir)); } + foreach (var dir in subDirs) { await CopyDirectory(fileListDir, modName, dir, convertFromTo(dir), shouldCorrectPaths); } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/AssemblyInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/AssemblyInfo.cs index 404e8b571..7a904fdac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/AssemblyInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/AssemblyInfo.cs @@ -1,7 +1,11 @@ using System; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; +[assembly:InternalsVisibleTo("WindowsTest"), + InternalsVisibleTo("MacTest"), + InternalsVisibleTo("LinuxTest")] public static class AssemblyInfo { public static readonly string GitRevision; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs index 2a1585e5b..eb17b94a8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs @@ -7,9 +7,14 @@ namespace Barotrauma { public static class ReflectionUtils { + private static Type[] cachedNonAbstractTypes; public static IEnumerable GetDerivedNonAbstract() { - return Assembly.GetEntryAssembly().GetTypes().Where(t => t.IsSubclassOf(typeof(T)) && !t.IsAbstract); + if (cachedNonAbstractTypes == null) + { + cachedNonAbstractTypes = Assembly.GetEntryAssembly().GetTypes().Where(t => !t.IsAbstract).ToArray(); + } + return cachedNonAbstractTypes.Where(t => t.IsSubclassOf(typeof(T))); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index b55e03a9f..365924358 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -480,7 +480,7 @@ namespace Barotrauma int read = 0; // FIXME workaround for .NET6 causing save decompression to fail -#if NET6_0 && LINUX +#if NET6_0 for (int i = 0; i < amount; i++) { int result = zipStream.ReadByte(); diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 37b6f8943..2942b8e72 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,87 @@ +--------------------------------------------------------------------------------------------------------- +v0.18.0.0 +--------------------------------------------------------------------------------------------------------- + +Balancing: +- Cargo mission reward of construction materials has been reduced to be less balance-breaking. +- Many fabrication and deconstruction recipes changed to prevent infinite construction loops. +- Reduced selling price to ~25% of base price to avoid getting too rich from looting too early/easily +- Increased effect of "Requested Goods" to be 2x to compensate for the decreased selling price. +- Alien artifacts and trinkets can still be sold for a high price at research stations. (2x modifier, to compensate for the reduced selling price) +- Removed batteries from Headset, to reduce the value of selling/deconstructing these. +- Duffelbag deteriorates over time when in use, and now is carried with both hands. +- All items now deconstruct into less materials than it takes to construct them. Avoiding infinite construction/deconstruction loops for easy skill leveling. +- Revisited all item spawns (WIP). Drastically reduced and adjusted the spawns everywhere. Disabled some spawns in campaign. All the subs should now start with a bare minimum in the campaign. +- (Temporarily?) Removed most hand-placed items from the vanilla subs to make balancing and debugging the auto item placement easier. +- Revisited crew corpse spawns. The id cards are no longer manually placed. The cards found from the crew now actually work. +- Minor adjustments to bandit loadouts. +- Changes to chaingun. Now fires 500 shots instead of 200 per ammo box, at the cost of DPS. +- Added shredder rounds for chaingun, as an option against armoured targets. +- Adjusted the armor penetration of all turrets. +- Made location evolution take a little longer, colonies cannot be formed closer than three steps to another colony. +- Made wreck missions a little more common. + +Changes and additions: +- Added damage overlays to characters (characters who've taken damage look damaged). +- Show a verification prompt if an automated circuit tries to make the submarine undock from or dock with an outpost. Prevents campaign getting softlocked if someone rewires the docking port in a way that makes it dock/undock immediately at the start of around. +- Color subs in the sub editor's list to indicate whether they're vanilla, workshop or local subs, added a tooltip that explains why some of them cannot be deleted through the editor. +- Optimized AI pathfinding when they're trying to find a safe hull. Particularly noticeable in colonies when the NPCs are fleeing from something. +- Optimized character status effects (e.g. health regen and other constant damage reductions). +- Optimized watcher's acid clouds. +- Optimized loading submarines. Reduces loading times especially when there's lots of items in the sub. +- ID cards can now be purchased from outposts. The card gets assigned the appropriate tags for the character doing the purchase. +- Clients need to wait 1 minute if their vote gets rejected before they can start another vote of the same type. +- Increased the priority of explosion particles to make it less likely for them to not appear when the particle limit has been reached. +- Made matriarch genes slowly heal bleeding (not just afflictions of the type "damage") to get it to be more in line with the description. +- Adjusted small water flow sounds: lower max volume, lerp volume according to the water flow (-> small leaks are much more quiet). +- Added energy drinks and protein bars to vending machines. +- Reduced Winterhalter engine power drain (from 6000 total to 4250). +- Decorative level objects (plants and whatnot) can spawn on outpost walls. +- Adjustments on the particle effects of chaingun and coilgun. +- Added non-lethal rubber bullets for riot shotgun. +- Added a server setting to change if the looted money goes to the player or to the bank. +- Improved tooltips in the wallet menu to make their function more clear. +- Corpses can now be grabbed in singleplayer to loot money. +- Made the crew wallet menu update when the players permissions change. +- Prevented selling items from submarine containers tagged with "donttakeitems", instead of "donttakeitems". +- Removed merchant balance effect on item prices. +- Replaced "item sell value" with the location reputation effect on the store interface. + +Fixes: +- Fixed an issue where the client was adding mission rewards into the bank on their screen causing desync. +- Fixed item sets failing to load when the system language is set to Turkish, causing NPCs to spawn without any items. +- Fixed ballast flora sometimes becoming unkillable in multiplayer. +- Attempt to fix tab menu crew list sometimes getting stuck to a broken state at the beginning of a round. +- Fixed inability to access the character tab in the tab menu when dead (preventing you from creating a new character). +- Fixed occasional "hash calculation for content package xxxx didn't match expected hash" errors when updating/enabling certain mods. +- Fixed preview sometimes breaking in the character customization menu when switching the hair or accessories on Linux or Mac. +- Fixed fonts not getting rescaled when changing resolution. +- Fixed misplaced hull in the beacon stations. +- Fixed ability to pick up items and take items from other characters when controlling a character whose inventory is inaccessible while alive. +- Fixed message box about a too large preview image not being shown when trying to publish one in the Workshop (instead throwing the generic "publishing failed" error). +- Fixed Venture airlock (missing button, inner door wiring). +- Fixed level floor not being visible on the sonar. +- Fixed bots being unable to shoot with a turret whose line of sight is blocked by another turret (even though the projectiles can go through the turret). +- Fixed switching a sub making its preview image disappear from the submarine switch menu. +- Fixed item assemblies still getting misaligned when saving. +- Fixed crashing when there's no audio device available (no speakers/headset connected) and a character enters water. +- Fixed crashing when trying to save an item assembly with a space at the end of the name. +- Fixed crashing when a character tries to operate a turret from outside the sub. +- Fixed submarine name being set to a truncated value in the submarine save dialog if the submarine name text at the top of the screen gets truncated, leading to a crash if you try to save the sub with that name. +- Fixed devices whose power consumption is set to 0 not working when not connected to a grid. +- Fixed outpost NPCs choosing the item to spawn for the device they're operating randomly, occasionally causing them to for example load reactors with volatile rods. +- Clients replicate sending chat messages to wifi components in mp. Fixes radio-linked wifi components not receiving the signals client-side. +- Fixed tab menu staying open during loading screens. +- Signal components' and terminals' sprites don't mirror horizontally in mirrored subs (what's a DNA, RO, ROX or XEGER component??). +- Fixed inability to rewire any docking ports in outpost levels, even if the port is not docked with anything (should only apply to the port docked with the outpost). +- Fixed "Ignore This" orders being wiped when loading an existing multiplayer campaign save. + +Modding: +- Fixed permanent stats given by talents not getting synced to clients in multiplayer (doesn't affect any vanilla talents). +- Fixed nullref exception when trying to trigger a location type change to a type that doesn't exist (doesn't happen in the vanilla game). +- Added an extra tag to the "canned heat" talent to make it easier to add custom upgradeable tanks that aren't compatible with vanilla tools. +- Option to make status effects drop the items contained inside the target item (usage example in the duffel bag). + --------------------------------------------------------------------------------------------------------- v0.17.15.0 --------------------------------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaShared/serversettings.xml b/Barotrauma/BarotraumaShared/serversettings.xml index 34f4c94b2..50ed25dac 100644 --- a/Barotrauma/BarotraumaShared/serversettings.xml +++ b/Barotrauma/BarotraumaShared/serversettings.xml @@ -52,4 +52,6 @@ missiontype="Random" autobantime="3600" maxautobantime="86400" - /> \ No newline at end of file + lootedmoneydestination="Bank" + maximumtransferrequest="999999" +/> \ No newline at end of file diff --git a/Barotrauma/BarotraumaTest/INetSerializableStructTests.cs b/Barotrauma/BarotraumaTest/INetSerializableStructTests.cs new file mode 100644 index 000000000..83cedcc37 --- /dev/null +++ b/Barotrauma/BarotraumaTest/INetSerializableStructTests.cs @@ -0,0 +1,205 @@ +#nullable enable + +using System; +using Barotrauma; +using Barotrauma.Networking; +using FluentAssertions; +using FsCheck; +using Microsoft.Xna.Framework; +using Xunit; + +namespace TestProject +{ + // ReSharper disable UnusedMember.Local NotAccessedField.Local UnusedMember.Global + public class INetSerializableStructTests + { + private class CustomGenerators + { + // no null strings!!! + public static Arbitrary StringGeneratorOverride() => Arb.Default.String().Generator.Where(s => s != null).ToArbitrary(); + } + + public INetSerializableStructTests() + { + Arb.Register(); + Arb.Register(); + } + + [Fact] + public void TestOptional() + { + Prop.ForAll>(SerializeDeserialize).QuickCheckThrowOnFailure(); + Prop.ForAll>(SerializeDeserialize).QuickCheckThrowOnFailure(); + Prop.ForAll>(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestNested() + { + Prop.ForAll((arg1, arg2, arg3) => SerializeDeserializeNullableTuple(arg1, new TupleNullableStruct { One = arg2, Two = arg3 })).QuickCheckThrowOnFailure(); + Prop.ForAll((arg1, arg2) => SerializeDeserialize(new TupleNullableStruct { One = arg1, Two = arg2 })).QuickCheckThrowOnFailure(); + Prop.ForAll((arg1, arg2) => SerializeDeserialize(new TupleNullableStruct { One = arg1, Two = arg2 })).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestVector2() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestColor() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestEnum() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestArray() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestNullable() + { + Prop.ForAll(SerializeDeserializeNullableTuple).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestBoolean() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestByte() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestUInt16() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestInt16() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestUInt32() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestInt32() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestUInt64() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestInt64() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestSingle() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestDouble() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + [Fact] + public void TestString() + { + Prop.ForAll(SerializeDeserialize).QuickCheckThrowOnFailure(); + } + + private enum EnumTest + { + One = 1, + Two = 2, + Three = 3, + Thousand = 1000 + } + + private struct TestStruct : INetSerializableStruct + { + [NetworkSerialize] + public T Value; + + public T NotSerializedValue; + + public T NotSerializedFunction() => throw new NotImplementedException(); + } + + private struct TupleNullableStruct : INetSerializableStruct + { + [NetworkSerialize] + public T? One; + + [NetworkSerialize] + public U? Two; + + public (T, U) NotSerializedValue; + public (T, U) NotSerializedFunction() => throw new NotImplementedException(); + } + + private static void SerializeDeserialize(T arg) where T : notnull + { + ReadWriteMessage msg = new ReadWriteMessage(); + TestStruct writeStruct = new TestStruct + { + Value = arg + }; + + ((INetSerializableStruct)writeStruct).Write(msg); + msg.BitPosition = 0; + + TestStruct readStruct = INetSerializableStruct.Read>(msg); + + readStruct.Should().BeEquivalentTo(writeStruct, options => options.ComparingByMembers>()); + } + + private static void SerializeDeserializeNullableTuple(T arg1, U arg2) + { + ReadWriteMessage msg = new ReadWriteMessage(); + TupleNullableStruct writeStruct = new TupleNullableStruct + { + One = arg1, + Two = arg2 + }; + + ((INetSerializableStruct)writeStruct).Write(msg); + msg.BitPosition = 0; + + TupleNullableStruct readStruct = INetSerializableStruct.Read>(msg); + + readStruct.Should().BeEquivalentTo(writeStruct, options => options.ComparingByMembers>()); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaTest/LinuxTest.csproj b/Barotrauma/BarotraumaTest/LinuxTest.csproj new file mode 100644 index 000000000..821101d06 --- /dev/null +++ b/Barotrauma/BarotraumaTest/LinuxTest.csproj @@ -0,0 +1,35 @@ + + + + net6.0 + enable + + false + + LinuxTest + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + diff --git a/Barotrauma/BarotraumaTest/MacTest.csproj b/Barotrauma/BarotraumaTest/MacTest.csproj new file mode 100644 index 000000000..20b02e7a3 --- /dev/null +++ b/Barotrauma/BarotraumaTest/MacTest.csproj @@ -0,0 +1,35 @@ + + + + net6.0 + enable + + false + + MacTest + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + diff --git a/Barotrauma/BarotraumaTest/TestExample.cs b/Barotrauma/BarotraumaTest/TestExample.cs new file mode 100644 index 000000000..6c02360dd --- /dev/null +++ b/Barotrauma/BarotraumaTest/TestExample.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Xml.Linq; +using Barotrauma; +using FluentAssertions; +using FsCheck; +using FsCheck.Xunit; +using Microsoft.Xna.Framework; +using Xunit; +using Xunit.Abstractions; + +namespace TestProject +{ + public class TestExample + { + // By default FsCheck has generators for basic types like floats ints and strings + // Anything custom like Rectangle or Vector2 requires writing a custom generator for it + private class CustomExampleGenerators + { + // We override the float generator to exclude NaNs and infinites + public static Arbitrary FloatGeneratorOverride() => Arb.Default.Float32().Generator.Where(MathUtils.IsValid).ToArbitrary(); + + // We override the String generator to exclude null and empty strings + public static Arbitrary StringGeneratorOverride() => Arb.Default.String().Generator.Where(s => !string.IsNullOrWhiteSpace(s)).ToArbitrary(); + + // Generator for the Rectangle type + public static Arbitrary RectangleGenerator() + { + return Arb.From(from int x in Arb.Generate() + from int y in Arb.Generate() + from int w in Arb.Generate().Where(i => i > 0) + from int h in Arb.Generate().Where(i => i > 0) + select new Rectangle(x, y, w, h)); + } + } + + // Used to output text into the test output + private readonly ITestOutputHelper testOutputHelper; + + public TestExample(ITestOutputHelper testOutputHelper) + { + this.testOutputHelper = testOutputHelper; + Arb.Register(); // Register our custom generators + } + + [Fact] // Create a public function and add the [Fact] attribute on it to make a test function + public void TestXORAlgorithm() + { + Prop.ForAll((text, key) => // generates a pair of random strings + { + string encrypted = XOREncryptDecrypt(text, key); + string decrypted = XOREncryptDecrypt(encrypted, key); + + decrypted.Should().BeEquivalentTo(text); // FluentAssertions provides clear and verbose assertions with the Should() method + }).VerboseCheckThrowOnFailure(testOutputHelper); + // VerboseCheck performs 100 tests and outputs the generated values into the test output + // ThrowOnFailure will additionally throw an exception if any of the functions fail which will make the test fail + + // We will see that this fails the test with the following exception: + + /* + * System.Exception + * Falsifiable, after 1 test (0 shrinks) (StdGen (2118948508,297004609)): + * Original: + * ("Jl", "m") + * with exception: + * Xunit.Sdk.XunitException: Expected decrypted to be equivalent to "Jl" with a length of 2, but "948492" has a length of 6, differs near "948" (index 0). + */ + + // We can see that the reason it is failing is because the original text was "Jl" but when encrypted and decrypted the string becomes "948492" + // This is of course because we are not casting the XOR'd value to a char and instead appending the integer to the string builder + } + + // Erroneous XOR encryption algorithm + private static string XOREncryptDecrypt(string text, string key) + { + var result = new StringBuilder(); + + for (int i = 0; i < text.Length; i++) + { + result.Append(text[i] ^ (uint)key[i % key.Length]); + } + + return result.ToString(); + } + + private class ExampleEntity : ISerializableEntity + { + [Serialize(0.0f, IsPropertySaveable.Yes)] + public float ExampleValue { get; set; } + + public string Name => nameof(ExampleEntity); + public Dictionary SerializableProperties { get; } + + public ExampleEntity() + { + SerializableProperties = SerializableProperty.GetProperties(this); + } + } + + [Fact] + public void TestPropertyConditionalEqualsOperator() + { + // Test if the PropertyConditional equals operator is working correctly + Prop.ForAll(value => + { + XAttribute xmlAttribute = new XAttribute("examplevalue", $"equals {value}"); + + PropertyConditional conditional = new PropertyConditional(xmlAttribute); + + ExampleEntity entity = new ExampleEntity + { + ExampleValue = value + }; + + conditional.Matches(entity).Should().BeTrue(); + }).VerboseCheckThrowOnFailure(testOutputHelper); // Remember to pass testOutputHelper so we actually get output + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaTest/TestProject.cs b/Barotrauma/BarotraumaTest/TestProject.cs new file mode 100644 index 000000000..614b0b23b --- /dev/null +++ b/Barotrauma/BarotraumaTest/TestProject.cs @@ -0,0 +1,34 @@ +using Barotrauma; +using FsCheck; +using Microsoft.Xna.Framework; + +namespace TestProject +{ + public static class TestProject + { + public class CustomGenerators + { + public static Arbitrary Vector2Generator() + { + return Arb.From(from int x in Arb.Generate() + from int y in Arb.Generate() + select new Vector2(x, y)); + } + + public static Arbitrary ColorGenerator() + { + return Arb.From(from int r in Gen.Choose(0, 255) + from int g in Gen.Choose(0, 255) + from int b in Gen.Choose(0, 255) + select new Color(r, g, b)); + } + + public static Arbitrary> OptionalGenerator() + { + return Arb.From(from T x in Arb.Generate() + from bool isNone in Arb.Generate() + select x is null || isNone ? Option.None() : Option.Some(x)); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaTest/WindowsTest.csproj b/Barotrauma/BarotraumaTest/WindowsTest.csproj new file mode 100644 index 000000000..f7c2fc3be --- /dev/null +++ b/Barotrauma/BarotraumaTest/WindowsTest.csproj @@ -0,0 +1,35 @@ + + + + net6.0 + enable + + false + + WindowsTest + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + diff --git a/Libraries/Farseer Physics Engine 3.5/Common/PathManager.cs b/Libraries/Farseer Physics Engine 3.5/Common/PathManager.cs index ac937096b..21e218c7b 100644 --- a/Libraries/Farseer Physics Engine 3.5/Common/PathManager.cs +++ b/Libraries/Farseer Physics Engine 3.5/Common/PathManager.cs @@ -44,13 +44,13 @@ namespace FarseerPhysics.Common if (path.Closed) { ChainShape chain = new ChainShape(verts, true); - body.CreateFixture(chain); + body.CreateFixture(chain, Category.Cat1, Category.All); } else { for (int i = 1; i < verts.Count; i++) { - body.CreateFixture(new EdgeShape(verts[i], verts[i - 1])); + body.CreateFixture(new EdgeShape(verts[i], verts[i - 1]), Category.Cat1, Category.All); } } } @@ -74,7 +74,7 @@ namespace FarseerPhysics.Common foreach (Vertices item in decomposedVerts) { - body.CreateFixture(new PolygonShape(item, density)); + body.CreateFixture(new PolygonShape(item, density), Category.Cat1, Category.All); } } @@ -105,7 +105,7 @@ namespace FarseerPhysics.Common foreach (Shape shape in shapes) { - b.CreateFixture(shape); + b.CreateFixture(shape, Category.Cat1, Category.All); } bodyList.Add(b); diff --git a/Libraries/Farseer Physics Engine 3.5/Common/PhysicsLogic/BreakableBody.cs b/Libraries/Farseer Physics Engine 3.5/Common/PhysicsLogic/BreakableBody.cs index 9f367a27a..4d988c318 100644 --- a/Libraries/Farseer Physics Engine 3.5/Common/PhysicsLogic/BreakableBody.cs +++ b/Libraries/Farseer Physics Engine 3.5/Common/PhysicsLogic/BreakableBody.cs @@ -56,7 +56,7 @@ namespace FarseerPhysics.Common.PhysicsLogic foreach (Vertices part in vertices) { PolygonShape polygonShape = new PolygonShape(part, density); - Fixture fixture = MainBody.CreateFixture(polygonShape); + Fixture fixture = MainBody.CreateFixture(polygonShape, Category.Cat1, Category.All); Parts.Add(fixture); } } @@ -67,7 +67,7 @@ namespace FarseerPhysics.Common.PhysicsLogic foreach (Shape part in shapes) { - Fixture fixture = MainBody.CreateFixture(part); + Fixture fixture = MainBody.CreateFixture(part, Category.Cat1, Category.All); Parts.Add(fixture); } } @@ -82,7 +82,7 @@ namespace FarseerPhysics.Common.PhysicsLogic foreach (Vertices part in triangles) { PolygonShape polygonShape = new PolygonShape(part, density); - Fixture fixture = MainBody.CreateFixture(polygonShape); + Fixture fixture = MainBody.CreateFixture(polygonShape, Category.Cat1, Category.All); Parts.Add(fixture); } } @@ -161,7 +161,7 @@ namespace FarseerPhysics.Common.PhysicsLogic Body body = World.CreateBody(MainBody.Position, MainBody.Rotation, BodyType.Dynamic); body.UserData = MainBody.UserData; - Fixture newFixture = body.CreateFixture(shape); + Fixture newFixture = body.CreateFixture(shape, Category.Cat1, Category.All); newFixture.UserData = fixtureTag; Parts[i] = newFixture; diff --git a/Libraries/Farseer Physics Engine 3.5/Common/Serialization.cs b/Libraries/Farseer Physics Engine 3.5/Common/Serialization.cs index 35f6535b9..5df05e24c 100644 --- a/Libraries/Farseer Physics Engine 3.5/Common/Serialization.cs +++ b/Libraries/Farseer Physics Engine 3.5/Common/Serialization.cs @@ -614,7 +614,7 @@ namespace FarseerPhysics.Common { foreach (XMLFragmentElement element in fixtureElement.Elements) { - Fixture fixture = new Fixture(); + Fixture fixture = new Fixture(Category.Cat1, Category.All); if (element.Name.ToLower() != "fixture") throw new Exception(); diff --git a/Libraries/Farseer Physics Engine 3.5/Content/BodyContainer.cs b/Libraries/Farseer Physics Engine 3.5/Content/BodyContainer.cs index 02ed6f58f..67dd7621c 100644 --- a/Libraries/Farseer Physics Engine 3.5/Content/BodyContainer.cs +++ b/Libraries/Farseer Physics Engine 3.5/Content/BodyContainer.cs @@ -35,7 +35,7 @@ namespace FarseerPhysics.Content foreach (FixtureTemplate fixtureTemplate in Fixtures) { - Fixture fixture = body.CreateFixture(fixtureTemplate.Shape); + Fixture fixture = body.CreateFixture(fixtureTemplate.Shape, Category.Cat1, Category.All); fixture.UserData = fixtureTemplate.Name; fixture.Restitution = fixtureTemplate.Restitution; fixture.Friction = fixtureTemplate.Friction; diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.Factory.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.Factory.cs index 69769aabe..389c100c1 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.Factory.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.Factory.cs @@ -27,76 +27,76 @@ namespace FarseerPhysics.Dynamics /// The shape. /// Application specific data /// - public virtual Fixture CreateFixture(Shape shape) + public virtual Fixture CreateFixture(Shape shape, Category collisionCategory, Category collidesWith) { - Fixture fixture = new Fixture(shape); + Fixture fixture = new Fixture(shape, collisionCategory, collidesWith); Add(fixture); return fixture; } - public Fixture CreateEdge(Vector2 start, Vector2 end) + public Fixture CreateEdge(Vector2 start, Vector2 end, Category collisionCategory, Category collidesWith) { EdgeShape edgeShape = new EdgeShape(start, end); - return CreateFixture(edgeShape); + return CreateFixture(edgeShape, collisionCategory, collidesWith); } - public Fixture CreateChainShape(Vertices vertices) + public Fixture CreateChainShape(Vertices vertices, Category collisionCategory, Category collidesWith) { ChainShape shape = new ChainShape(vertices); - return CreateFixture(shape); + return CreateFixture(shape, collisionCategory, collidesWith); } - public Fixture CreateLoopShape(Vertices vertices) + public Fixture CreateLoopShape(Vertices vertices, Category collisionCategory, Category collidesWith) { ChainShape shape = new ChainShape(vertices, true); - return CreateFixture(shape); + return CreateFixture(shape, collisionCategory, collidesWith); } - public Fixture CreateRectangle(float width, float height, float density, Vector2 offset) + public Fixture CreateRectangle(float width, float height, float density, Vector2 offset, Category collisionCategory, Category collidesWith) { Vertices rectangleVertices = PolygonTools.CreateRectangle(width / 2, height / 2); rectangleVertices.Translate(ref offset); PolygonShape rectangleShape = new PolygonShape(rectangleVertices, density); - return CreateFixture(rectangleShape); + return CreateFixture(rectangleShape, collisionCategory, collidesWith); } - public Fixture CreateRectangle(float width, float height, float density, float rotation, Vector2 offset) + public Fixture CreateRectangle(float width, float height, float density, float rotation, Vector2 offset, Category collisionCategory, Category collidesWith) { Vertices rectangleVertices = PolygonTools.CreateRectangle(width / 2, height / 2, Vector2.Zero, rotation); rectangleVertices.Translate(ref offset); PolygonShape rectangleShape = new PolygonShape(rectangleVertices, density); - return CreateFixture(rectangleShape); + return CreateFixture(rectangleShape, collisionCategory, collidesWith); } - public Fixture CreateCircle(float radius, float density) + public Fixture CreateCircle(float radius, float density, Category collisionCategory, Category collidesWith) { if (radius <= 0) throw new ArgumentOutOfRangeException("radius", "Radius must be more than 0 meters"); CircleShape circleShape = new CircleShape(radius, density); - return CreateFixture(circleShape); + return CreateFixture(circleShape, collisionCategory, collidesWith); } - public Fixture CreateCircle(float radius, float density, Vector2 offset) + public Fixture CreateCircle(float radius, float density, Vector2 offset, Category collisionCategory, Category collidesWith) { if (radius <= 0) throw new ArgumentOutOfRangeException("radius", "Radius must be more than 0 meters"); CircleShape circleShape = new CircleShape(radius, density); circleShape.Position = offset; - return CreateFixture(circleShape); + return CreateFixture(circleShape, collisionCategory, collidesWith); } - public Fixture CreatePolygon(Vertices vertices, float density) + public Fixture CreatePolygon(Vertices vertices, float density, Category collisionCategory, Category collidesWith) { if (vertices.Count <= 1) throw new ArgumentOutOfRangeException("vertices", "Too few points to be a polygon"); PolygonShape polygon = new PolygonShape(vertices, density); - return CreateFixture(polygon); + return CreateFixture(polygon, collisionCategory, collidesWith); } - public Fixture CreateEllipse(float xRadius, float yRadius, int edges, float density) + public Fixture CreateEllipse(float xRadius, float yRadius, int edges, float density, Category collisionCategory, Category collidesWith) { if (xRadius <= 0) throw new ArgumentOutOfRangeException("xRadius", "X-radius must be more than 0"); @@ -106,10 +106,10 @@ namespace FarseerPhysics.Dynamics Vertices ellipseVertices = PolygonTools.CreateEllipse(xRadius, yRadius, edges); PolygonShape polygonShape = new PolygonShape(ellipseVertices, density); - return CreateFixture(polygonShape); + return CreateFixture(polygonShape, collisionCategory, collidesWith); } - public List CreateCompoundPolygon(List list, float density) + public List CreateCompoundPolygon(List list, float density, Category collisionCategory, Category collidesWith) { List res = new List(list.Count); @@ -119,26 +119,26 @@ namespace FarseerPhysics.Dynamics if (vertices.Count == 2) { EdgeShape shape = new EdgeShape(vertices[0], vertices[1]); - res.Add(CreateFixture(shape)); + res.Add(CreateFixture(shape, collisionCategory, collidesWith)); } else { PolygonShape shape = new PolygonShape(vertices, density); - res.Add(CreateFixture(shape)); + res.Add(CreateFixture(shape, collisionCategory, collidesWith)); } } return res; } - public Fixture CreateLineArc(float radians, int sides, float radius, bool closed) + public Fixture CreateLineArc(float radians, int sides, float radius, bool closed, Category collisionCategory, Category collidesWith) { Vertices arc = PolygonTools.CreateArc(radians, sides, radius); arc.Rotate((MathHelper.Pi - radians) / 2); - return closed ? CreateLoopShape(arc) : CreateChainShape(arc); + return closed ? CreateLoopShape(arc, collisionCategory, collidesWith) : CreateChainShape(arc, collisionCategory, collidesWith); } - public List CreateSolidArc(float density, float radians, int sides, float radius) + public List CreateSolidArc(float density, float radians, int sides, float radius, Category collisionCategory, Category collidesWith) { Vertices arc = PolygonTools.CreateArc(radians, sides, radius); arc.Rotate((MathHelper.Pi - radians) / 2); @@ -148,7 +148,7 @@ namespace FarseerPhysics.Dynamics List triangles = Triangulate.ConvexPartition(arc, TriangulationAlgorithm.Earclip); - return CreateCompoundPolygon(triangles, density); + return CreateCompoundPolygon(triangles, density, collisionCategory, collidesWith); } } } \ No newline at end of file diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/Fixture.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/Fixture.cs index 40381f0d4..d65d9a1b0 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/Fixture.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/Fixture.cs @@ -82,10 +82,10 @@ namespace FarseerPhysics.Dynamics /// public OnSeparationEventHandler OnSeparation; - internal Fixture() // Note: This is internal because it's used by Deserialization. + internal Fixture(Category collisionCategory, Category collidesWith) // Note: This is internal because it's used by Deserialization. { - _collisionCategories = Category.Cat1; - _collidesWith = Category.All; + _collisionCategories = collisionCategory; + _collidesWith = collidesWith; _collisionGroup = 0; //Fixture defaults @@ -93,7 +93,7 @@ namespace FarseerPhysics.Dynamics Restitution = 0f; } - public Fixture(Shape shape) : this() + public Fixture(Shape shape, Category collisionCategory, Category collidesWith) : this(collisionCategory, collidesWith) { Shape = shape.Clone(); @@ -375,15 +375,15 @@ namespace FarseerPhysics.Dynamics /// The cloned fixture. internal Fixture CloneOnto(Body body, Shape shape) { - Fixture fixture = new Fixture(shape.Clone()); - fixture.UserData = UserData; - fixture.Restitution = Restitution; - fixture.Friction = Friction; - fixture.IsSensor = IsSensor; - fixture._collisionGroup = _collisionGroup; - fixture._collisionCategories = _collisionCategories; - fixture._collidesWith = _collidesWith; - + Fixture fixture = new Fixture(shape.Clone(), _collisionCategories, _collidesWith) + { + UserData = UserData, + Restitution = Restitution, + Friction = Friction, + IsSensor = IsSensor, + _collisionGroup = _collisionGroup + }; + body.Add(fixture); return fixture; } diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/World.Factory.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/World.Factory.cs index f3a778d24..b99ca20e6 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/World.Factory.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/World.Factory.cs @@ -17,43 +17,42 @@ namespace FarseerPhysics.Dynamics { public partial class World { - public virtual Body CreateBody(Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public virtual Body CreateBody(Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, bool findNewContacts = true) { - Body body = new Body(); - body.Position = position; - body.Rotation = rotation; - body.BodyType = bodyType; - - AddAsync(body); + Body body = new Body + { + Position = position, + Rotation = rotation, + BodyType = bodyType + }; + + AddAsync(body, findNewContacts); return body; } - public Body CreateEdge(Vector2 start, Vector2 end) + public Body CreateEdge(Vector2 start, Vector2 end, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All, bool findNewContacts = true) { - Body body = CreateBody(); - - body.CreateEdge(start, end); + Body body = CreateBody(bodyType: bodyType, findNewContacts: findNewContacts); + body.CreateEdge(start, end, collisionCategory, collidesWith); return body; } - public Body CreateChainShape(Vertices vertices, Vector2 position = new Vector2()) + public Body CreateChainShape(Vertices vertices, Vector2 position = new Vector2(), Category collisionCategory = Category.Cat1, Category collidesWith = Category.All, bool findNewContacts = true) { - Body body = CreateBody(position); - - body.CreateChainShape(vertices); + Body body = CreateBody(position, findNewContacts: findNewContacts); + body.CreateChainShape(vertices, collisionCategory, collidesWith); return body; } - public Body CreateLoopShape(Vertices vertices, Vector2 position = new Vector2()) + public Body CreateLoopShape(Vertices vertices, Vector2 position = new Vector2(), Category collisionCategory = Category.Cat1, Category collidesWith = Category.All, bool findNewContacts = true) { - Body body = CreateBody(position); - - body.CreateLoopShape(vertices); + Body body = CreateBody(position, findNewContacts: findNewContacts); + body.CreateLoopShape(vertices, collisionCategory, collidesWith); return body; } - public Body CreateRectangle(float width, float height, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public Body CreateRectangle(float width, float height, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All, bool findNewContacts = true) { if (width <= 0) throw new ArgumentOutOfRangeException("width", "Width must be more than 0 meters"); @@ -61,44 +60,44 @@ namespace FarseerPhysics.Dynamics if (height <= 0) throw new ArgumentOutOfRangeException("height", "Height must be more than 0 meters"); - Body body = CreateBody(position, rotation, bodyType); + Body body = CreateBody(position, rotation, bodyType, findNewContacts); Vertices rectangleVertices = PolygonTools.CreateRectangle(width / 2, height / 2); - body.CreatePolygon(rectangleVertices, density); + body.CreatePolygon(rectangleVertices, density, collisionCategory, collidesWith); return body; } - public Body CreateCircle(float radius, float density, Vector2 position = new Vector2(), BodyType bodyType = BodyType.Static) + public Body CreateCircle(float radius, float density, Vector2 position = new Vector2(), BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All, bool findNewContacts = true) { - Body body = CreateBody(position, 0, bodyType); - body.CreateCircle(radius, density); + Body body = CreateBody(position, 0, bodyType, findNewContacts); + body.CreateCircle(radius, density, collisionCategory, collidesWith); return body; } - public Body CreateEllipse(float xRadius, float yRadius, int edges, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public Body CreateEllipse(float xRadius, float yRadius, int edges, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All, bool findNewContacts = true) { - Body body = CreateBody(position, rotation, bodyType); - body.CreateEllipse(xRadius, yRadius, edges, density); + Body body = CreateBody(position, rotation, bodyType, findNewContacts); + body.CreateEllipse(xRadius, yRadius, edges, density, collisionCategory, collidesWith); return body; } - public Body CreatePolygon(Vertices vertices, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public Body CreatePolygon(Vertices vertices, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All, bool findNewContacts = true) { - Body body = CreateBody(position, rotation, bodyType); - body.CreatePolygon(vertices, density); + Body body = CreateBody(position, rotation, bodyType, findNewContacts); + body.CreatePolygon(vertices, density, collisionCategory, collidesWith); return body; } - public Body CreateCompoundPolygon(List list, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public Body CreateCompoundPolygon(List list, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All, bool findNewContacts = true) { //We create a single body - Body body = CreateBody(position, rotation, bodyType); - body.CreateCompoundPolygon(list, density); + Body body = CreateBody(position, rotation, bodyType, findNewContacts); + body.CreateCompoundPolygon(list, density, collisionCategory, collidesWith); return body; } - public Body CreateGear(float radius, int numberOfTeeth, float tipPercentage, float toothHeight, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public Body CreateGear(float radius, int numberOfTeeth, float tipPercentage, float toothHeight, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All) { Vertices gearPolygon = PolygonTools.CreateGear(radius, numberOfTeeth, tipPercentage, toothHeight); @@ -108,13 +107,13 @@ namespace FarseerPhysics.Dynamics //Decompose the gear: List list = Triangulate.ConvexPartition(gearPolygon, TriangulationAlgorithm.Earclip); - return CreateCompoundPolygon(list, density, position, rotation, bodyType); + return CreateCompoundPolygon(list, density, position, rotation, bodyType, collisionCategory, collidesWith); } - return CreatePolygon(gearPolygon, density, position, rotation, bodyType); + return CreatePolygon(gearPolygon, density, position, rotation, bodyType, collisionCategory, collidesWith); } - public Body CreateCapsule(float height, float topRadius, int topEdges, float bottomRadius, int bottomEdges, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public Body CreateCapsule(float height, float topRadius, int topEdges, float bottomRadius, int bottomEdges, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All, bool findNewContacts = true) { Vertices verts = PolygonTools.CreateCapsule(height, topRadius, topEdges, bottomRadius, bottomEdges); @@ -122,23 +121,25 @@ namespace FarseerPhysics.Dynamics if (verts.Count >= Settings.MaxPolygonVertices) { List vertList = Triangulate.ConvexPartition(verts, TriangulationAlgorithm.Earclip); - return CreateCompoundPolygon(vertList, density, position, rotation, bodyType); + return CreateCompoundPolygon(vertList, density, position, rotation, bodyType, collisionCategory, collidesWith, findNewContacts); } - return CreatePolygon(verts, density, position, rotation, bodyType); + return CreatePolygon(verts, density, position, rotation, bodyType, collisionCategory, collidesWith, findNewContacts); } - public Body CreateCapsuleHorizontal(float width, float endRadius, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public Body CreateCapsuleHorizontal(float width, float endRadius, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All, bool findNewContacts = true) { //Create the middle rectangle Vertices rectangle = PolygonTools.CreateRectangle(width / 2, endRadius); - List list = new List(); - list.Add(rectangle); + List list = new List + { + rectangle + }; - Body body = CreateCompoundPolygon(list, density, position, rotation, bodyType); - body.CreateCircle(endRadius, density, new Vector2(width / 2, 0)); - body.CreateCircle(endRadius, density, new Vector2(-width / 2, 0)); + Body body = CreateCompoundPolygon(list, density, position, rotation, bodyType, collisionCategory, collidesWith, findNewContacts); + body.CreateCircle(endRadius, density, new Vector2(width / 2, 0), collisionCategory, collidesWith); + body.CreateCircle(endRadius, density, new Vector2(-width / 2, 0), collisionCategory, collidesWith); //Create the two circles //CircleShape topCircle = new CircleShape(endRadius, density); @@ -150,17 +151,19 @@ namespace FarseerPhysics.Dynamics //body.CreateFixture(bottomCircle); return body; } - public Body CreateCapsule(float height, float endRadius, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public Body CreateCapsule(float height, float endRadius, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All, bool findNewContacts = true) { //Create the middle rectangle Vertices rectangle = PolygonTools.CreateRectangle(endRadius, height / 2); - List list = new List(); - list.Add(rectangle); + List list = new List() + { + rectangle + }; - Body body = CreateCompoundPolygon(list, density, position, rotation, bodyType); - body.CreateCircle(endRadius, density, new Vector2(0, height / 2)); - body.CreateCircle(endRadius, density, new Vector2(0, -(height / 2))); + Body body = CreateCompoundPolygon(list, density, position, rotation, bodyType, collisionCategory, collidesWith, findNewContacts); + body.CreateCircle(endRadius, density, new Vector2(0, height / 2), collisionCategory, collidesWith); + body.CreateCircle(endRadius, density, new Vector2(0, -(height / 2)), collisionCategory, collidesWith); //Create the two circles //CircleShape topCircle = new CircleShape(endRadius, density); @@ -173,7 +176,7 @@ namespace FarseerPhysics.Dynamics return body; } - public Body CreateRoundedRectangle(float width, float height, float xRadius, float yRadius, int segments, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public Body CreateRoundedRectangle(float width, float height, float xRadius, float yRadius, int segments, float density, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All) { Vertices verts = PolygonTools.CreateRoundedRectangle(width, height, xRadius, yRadius, segments); @@ -181,23 +184,23 @@ namespace FarseerPhysics.Dynamics if (verts.Count >= Settings.MaxPolygonVertices) { List vertList = Triangulate.ConvexPartition(verts, TriangulationAlgorithm.Earclip); - return CreateCompoundPolygon(vertList, density, position, rotation, bodyType); + return CreateCompoundPolygon(vertList, density, position, rotation, bodyType, collisionCategory, collidesWith); } - return CreatePolygon(verts, density, position, rotation, bodyType); + return CreatePolygon(verts, density, position, rotation, bodyType, collisionCategory, collidesWith); } - public Body CreateLineArc(float radians, int sides, float radius, bool closed = false, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public Body CreateLineArc(float radians, int sides, float radius, bool closed = false, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All) { Body body = CreateBody(position, rotation, bodyType); - body.CreateLineArc(radians, sides, radius, closed); + body.CreateLineArc(radians, sides, radius, closed, collisionCategory, collidesWith); return body; } - public Body CreateSolidArc(float density, float radians, int sides, float radius, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static) + public Body CreateSolidArc(float density, float radians, int sides, float radius, Vector2 position = new Vector2(), float rotation = 0, BodyType bodyType = BodyType.Static, Category collisionCategory = Category.Cat1, Category collidesWith = Category.All) { Body body = CreateBody(position, rotation, bodyType); - body.CreateSolidArc(density, radians, sides, radius); + body.CreateSolidArc(density, radians, sides, radius, collisionCategory, collidesWith); return body; } diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/World.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/World.cs index e8b8cbea2..0aa2ef49a 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/World.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/World.cs @@ -936,7 +936,7 @@ namespace FarseerPhysics.Dynamics /// Warning: This method is locked during callbacks. /// /// Thrown when the world is Locked/Stepping. - public virtual void Add(Body body) + public virtual void Add(Body body, bool findNewContacts) { if (IsLocked) throw new WorldLockedException("Cannot add bodies when the World is locked."); @@ -972,8 +972,10 @@ namespace FarseerPhysics.Dynamics if (Enabled) body.CreateProxies(); - ContactManager.FindNewContacts(); - + if (findNewContacts) + { + ContactManager.FindNewContacts(); + } // Fire World events: @@ -1227,7 +1229,7 @@ namespace FarseerPhysics.Dynamics /// Add a rigid body. /// /// - public void AddAsync(Body body) + public void AddAsync(Body body, bool findNewContacts) { if (body == null) throw new ArgumentNullException("body"); @@ -1243,7 +1245,7 @@ namespace FarseerPhysics.Dynamics Debug.WriteLine("You are adding the same body more than once."); } else - Add(body); + Add(body, findNewContacts); } /// @@ -1322,7 +1324,7 @@ namespace FarseerPhysics.Dynamics if (_bodyAddList.Count > 0) { foreach (Body body in _bodyAddList) - Add(body); + Add(body, findNewContacts: true); _bodyAddList.Clear(); } @@ -1414,7 +1416,7 @@ namespace FarseerPhysics.Dynamics ProcessChanges(); if (Settings.EnableDiagnostics) - AddRemoveTime = TimeSpan.FromTicks(_watch.ElapsedTicks); + AddRemoveTime = TimeSpan.FromTicks(_watch.Elapsed.Ticks); // If new fixtures were added, we need to find the new contacts. if (_worldHasNewFixture) @@ -1423,7 +1425,7 @@ namespace FarseerPhysics.Dynamics _worldHasNewFixture = false; } if (Settings.EnableDiagnostics) - NewContactsTime = TimeSpan.FromTicks(_watch.ElapsedTicks) - AddRemoveTime; + NewContactsTime = TimeSpan.FromTicks(_watch.Elapsed.Ticks) - AddRemoveTime; //FPE only: moved position and velocity iterations into Settings.cs TimeStep step; @@ -1443,12 +1445,12 @@ namespace FarseerPhysics.Dynamics ControllerList[i].Update(dt); } if (Settings.EnableDiagnostics) - ControllersUpdateTime = TimeSpan.FromTicks(_watch.ElapsedTicks) - (AddRemoveTime + NewContactsTime); + ControllersUpdateTime = TimeSpan.FromTicks(_watch.Elapsed.Ticks) - (AddRemoveTime + NewContactsTime); // Update contacts. This is where some contacts are destroyed. ContactManager.Collide(); if (Settings.EnableDiagnostics) - ContactsUpdateTime = TimeSpan.FromTicks(_watch.ElapsedTicks) - (AddRemoveTime + NewContactsTime + ControllersUpdateTime); + ContactsUpdateTime = TimeSpan.FromTicks(_watch.Elapsed.Ticks) - (AddRemoveTime + NewContactsTime + ControllersUpdateTime); // Integrate velocities, solve velocity constraints, and integrate positions. if (_stepComplete && step.dt > 0.0f) @@ -1456,7 +1458,7 @@ namespace FarseerPhysics.Dynamics Solve(ref step); } if (Settings.EnableDiagnostics) - SolveUpdateTime = TimeSpan.FromTicks(_watch.ElapsedTicks) - (AddRemoveTime + NewContactsTime + ControllersUpdateTime + ContactsUpdateTime); + SolveUpdateTime = TimeSpan.FromTicks(_watch.Elapsed.Ticks) - (AddRemoveTime + NewContactsTime + ControllersUpdateTime + ContactsUpdateTime); // Handle TOI events. if (Settings.ContinuousPhysics && step.dt > 0.0f) @@ -1464,7 +1466,7 @@ namespace FarseerPhysics.Dynamics SolveTOI(ref step, ref iterations); } if (Settings.EnableDiagnostics) - ContinuousPhysicsTime = TimeSpan.FromTicks(_watch.ElapsedTicks) - (AddRemoveTime + NewContactsTime + ControllersUpdateTime + ContactsUpdateTime + SolveUpdateTime); + ContinuousPhysicsTime = TimeSpan.FromTicks(_watch.Elapsed.Ticks) - (AddRemoveTime + NewContactsTime + ControllersUpdateTime + ContactsUpdateTime + SolveUpdateTime); if (step.dt > 0.0f) Fluid.Update(dt); diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture2D.OpenGL.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture2D.OpenGL.cs index 6e6d04d58..5ec3a2b63 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture2D.OpenGL.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture2D.OpenGL.cs @@ -35,6 +35,7 @@ namespace Microsoft.Xna.Framework.Graphics format.GetGLFormat(GraphicsDevice, out glInternalFormat, out glFormat, out glType); Threading.BlockOnUIThread(() => { + var prev = GraphicsExtensions.GetBoundTexture2D(); GenerateGLTextureIfRequired(); int w = width; int h = height; @@ -80,6 +81,8 @@ namespace Microsoft.Xna.Framework.Graphics h = h / 2; ++level; } + + GL.BindTexture(TextureTarget.Texture2D, prev); }); } diff --git a/LinuxSolution.sln b/LinuxSolution.sln index 073003f04..095528d41 100644 --- a/LinuxSolution.sln +++ b/LinuxSolution.sln @@ -39,6 +39,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LinuxServer", "Barotrauma\B EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoGame.Framework.Linux.NetStandard", "Libraries\MonoGame.Framework\Src\MonoGame.Framework\MonoGame.Framework.Linux.NetStandard.csproj", "{33E95A21-E071-4432-819F-AA64CF3EF3F1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LinuxTest", "Barotrauma\BarotraumaTest\LinuxTest.csproj", "{F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -181,6 +183,18 @@ Global {33E95A21-E071-4432-819F-AA64CF3EF3F1}.Unstable|Any CPU.Build.0 = Release|Any CPU {33E95A21-E071-4432-819F-AA64CF3EF3F1}.Unstable|x64.ActiveCfg = Unstable|x64 {33E95A21-E071-4432-819F-AA64CF3EF3F1}.Unstable|x64.Build.0 = Unstable|x64 + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Debug|x64.ActiveCfg = Debug|Any CPU + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Debug|x64.Build.0 = Debug|Any CPU + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Release|Any CPU.Build.0 = Release|Any CPU + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Release|x64.ActiveCfg = Release|Any CPU + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Release|x64.Build.0 = Release|Any CPU + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Unstable|Any CPU.Build.0 = Debug|Any CPU + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Unstable|x64.ActiveCfg = Debug|Any CPU + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}.Unstable|x64.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -198,6 +212,7 @@ Global {D47E4AAA-C3E5-4F0D-B7FF-D3B54966DE51} = {68B18BE6-9EE0-49DA-AE3A-4C7326F768F9} {2B0881F6-9C67-4446-A1F2-FC042763A462} = {68B18BE6-9EE0-49DA-AE3A-4C7326F768F9} {33E95A21-E071-4432-819F-AA64CF3EF3F1} = {DE36F45F-F09E-4719-B953-00D148F7722A} + {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3} = {68B18BE6-9EE0-49DA-AE3A-4C7326F768F9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {17032EAB-554B-4B44-A4F6-EFB177ACAB7A} diff --git a/MacSolution.sln b/MacSolution.sln index 162365f9c..20e699038 100644 --- a/MacSolution.sln +++ b/MacSolution.sln @@ -36,6 +36,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoGame.Framework.MacOS.Ne EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Facepunch.Steamworks.Posix", "Libraries\Facepunch.Steamworks\Facepunch.Steamworks.Posix.csproj", "{F10CE3BB-26B8-446E-84D2-86D25E850F61}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MacTest", "Barotrauma\BarotraumaTest\MacTest.csproj", "{20BC9336-B439-4BF1-8B65-D587DBF421D1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -178,6 +180,18 @@ Global {F10CE3BB-26B8-446E-84D2-86D25E850F61}.Unstable|Any CPU.Build.0 = Release|Any CPU {F10CE3BB-26B8-446E-84D2-86D25E850F61}.Unstable|x64.ActiveCfg = Release|Any CPU {F10CE3BB-26B8-446E-84D2-86D25E850F61}.Unstable|x64.Build.0 = Release|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Debug|x64.ActiveCfg = Debug|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Debug|x64.Build.0 = Debug|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Release|Any CPU.Build.0 = Release|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Release|x64.ActiveCfg = Release|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Release|x64.Build.0 = Release|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Unstable|Any CPU.Build.0 = Debug|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Unstable|x64.ActiveCfg = Debug|Any CPU + {20BC9336-B439-4BF1-8B65-D587DBF421D1}.Unstable|x64.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -195,6 +209,7 @@ Global {F17FB469-E9E6-4B1C-B887-4FE709D4D771} = {DFD82BBD-8D05-403D-BEBC-F4C1CF783E18} {35DDDA7D-328D-4A5D-BCBB-2E60C830A899} = {DE36F45F-F09E-4719-B953-00D148F7722A} {F10CE3BB-26B8-446E-84D2-86D25E850F61} = {DE36F45F-F09E-4719-B953-00D148F7722A} + {20BC9336-B439-4BF1-8B65-D587DBF421D1} = {DFD82BBD-8D05-403D-BEBC-F4C1CF783E18} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {17032EAB-554B-4B44-A4F6-EFB177ACAB7A} diff --git a/WindowsSolution.sln b/WindowsSolution.sln index 9c1bdeae7..515450184 100644 --- a/WindowsSolution.sln +++ b/WindowsSolution.sln @@ -39,6 +39,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XNATypes", "Libraries\XNATy EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpFont.NetStandard", "Libraries\SharpFont\Source\SharpFont\SharpFont.NetStandard.csproj", "{6911872D-40EF-400C-B0A1-9985A19ED488}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsTest", "Barotrauma\BarotraumaTest\WindowsTest.csproj", "{C7212AE2-A925-4225-A639-AE0653EF65B0}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution Libraries\GameAnalytics\GA-SDK-MONO-SHARED\GA-SDK-MONO-SHARED.projitems*{95c4d59d-9be4-4278-b4f8-46c0ba1a3916}*SharedItemsImports = 5 @@ -115,6 +117,12 @@ Global {6911872D-40EF-400C-B0A1-9985A19ED488}.Release|x64.Build.0 = Release|x64 {6911872D-40EF-400C-B0A1-9985A19ED488}.Unstable|x64.ActiveCfg = Release|x64 {6911872D-40EF-400C-B0A1-9985A19ED488}.Unstable|x64.Build.0 = Release|x64 + {C7212AE2-A925-4225-A639-AE0653EF65B0}.Debug|x64.ActiveCfg = Debug|Any CPU + {C7212AE2-A925-4225-A639-AE0653EF65B0}.Debug|x64.Build.0 = Debug|Any CPU + {C7212AE2-A925-4225-A639-AE0653EF65B0}.Release|x64.ActiveCfg = Release|Any CPU + {C7212AE2-A925-4225-A639-AE0653EF65B0}.Release|x64.Build.0 = Release|Any CPU + {C7212AE2-A925-4225-A639-AE0653EF65B0}.Unstable|x64.ActiveCfg = Debug|Any CPU + {C7212AE2-A925-4225-A639-AE0653EF65B0}.Unstable|x64.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -132,6 +140,7 @@ Global {47848C6E-C7A8-4EC3-96C2-3BC8A4234AFA} = {78A9F0AA-5519-407A-9B72-2A09F5DF7068} {1F318AC4-F808-4130-867F-B98DF9AA8F95} = {DE36F45F-F09E-4719-B953-00D148F7722A} {6911872D-40EF-400C-B0A1-9985A19ED488} = {DE36F45F-F09E-4719-B953-00D148F7722A} + {C7212AE2-A925-4225-A639-AE0653EF65B0} = {78A9F0AA-5519-407A-9B72-2A09F5DF7068} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {17032EAB-554B-4B44-A4F6-EFB177ACAB7A} From d4f6f4cf88bcefcb8c09a3621ab20b2ff26a6558 Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Fri, 13 May 2022 22:55:07 +0900 Subject: [PATCH 02/14] Build 0.18.1.0 --- .../BarotraumaClient/ClientSource/GUI/GUI.cs | 3 +- .../ClientSource/GUI/GUIContextMenu.cs | 4 +- .../ClientSource/GUI/TabMenu.cs | 6 +- .../GameModes/Tutorials/CaptainTutorial.cs | 19 +- .../GameModes/Tutorials/DoctorTutorial.cs | 11 + .../GameModes/Tutorials/EngineerTutorial.cs | 11 + .../GameModes/Tutorials/MechanicTutorial.cs | 18 ++ .../GameModes/Tutorials/OfficerTutorial.cs | 14 ++ .../GameModes/Tutorials/ScenarioTutorial.cs | 4 + .../ClientSource/Networking/GameClient.cs | 1 + .../ClientSource/Networking/ServerSettings.cs | 2 +- .../BarotraumaClient/LinuxClient.csproj | 2 +- .../BarotraumaClient/LinuxClient.csproj.bak | 208 +++++++++++++++ Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/MacClient.csproj.bak | 214 ++++++++++++++++ .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaClient/WindowsClient.csproj.bak | 237 ++++++++++++++++++ .../BarotraumaServer/LinuxServer.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj.bak | 156 ++++++++++++ Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../BarotraumaServer/MacServer.csproj.bak | 157 ++++++++++++ .../GameModes/MultiPlayerCampaign.cs | 2 +- .../ServerSource/Networking/GameServer.cs | 1 + .../BarotraumaServer/WindowsServer.csproj | 2 +- .../BarotraumaServer/WindowsServer.csproj.bak | 152 +++++++++++ .../GameSession/AutoItemPlacer.cs | 6 +- .../Items/Components/ItemComponent.cs | 4 +- .../SharedSource/Map/Levels/Level.cs | 20 ++ .../Map/Levels/LevelGenerationParams.cs | 8 +- .../SharedSource/Networking/ServerSettings.cs | 2 +- .../Serialization/SerializableProperty.cs | 2 +- Barotrauma/BarotraumaShared/changelog.txt | 14 +- 32 files changed, 1263 insertions(+), 25 deletions(-) create mode 100644 Barotrauma/BarotraumaClient/LinuxClient.csproj.bak create mode 100644 Barotrauma/BarotraumaClient/MacClient.csproj.bak create mode 100644 Barotrauma/BarotraumaClient/WindowsClient.csproj.bak create mode 100644 Barotrauma/BarotraumaServer/LinuxServer.csproj.bak create mode 100644 Barotrauma/BarotraumaServer/MacServer.csproj.bak create mode 100644 Barotrauma/BarotraumaServer/WindowsServer.csproj.bak diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 4b2e186a8..8d65228ff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -2425,7 +2425,8 @@ namespace Barotrauma verificationTextTag: GameMain.GameSession == null ? "PauseMenuQuitVerificationEditor" : "PauseMenuQuitVerification", action: () => { - GameMain.QuitToMainMenu(save: false); + // In the first campaign round we need to save the start items. + GameMain.QuitToMainMenu(save: GameMain.GameSession.GameMode is SinglePlayerCampaign campaign && campaign.IsFirstRound); }); } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs index eeebb32c4..8b8e09f16 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs @@ -290,7 +290,7 @@ namespace Barotrauma public override void AddToGUIUpdateList(bool ignoreChildren = false, int order = 0) { base.AddToGUIUpdateList(ignoreChildren, order); - SubMenu?.AddToGUIUpdateList(); + SubMenu?.AddToGUIUpdateList(order: 2); } public static void AddActiveToGUIUpdateList() @@ -300,7 +300,7 @@ namespace Barotrauma CurrentContextMenu = null; } - CurrentContextMenu?.AddToGUIUpdateList(); + CurrentContextMenu?.AddToGUIUpdateList(order: 2); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 87b4b2e92..d40a43dc0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -67,7 +67,7 @@ namespace Barotrauma this.frame = frame; this.permissionIcon = permissionIcon; character = client?.Character; - wasCharacterAlive = client.Character != null && !client.Character.IsDead; + wasCharacterAlive = client?.Character != null && !client.Character.IsDead; } public LinkedGUI(Character character, GUIFrame frame, GUITextBlock textBlock) @@ -1253,8 +1253,8 @@ namespace Barotrauma if (!hasMoneyPermissions && GameMain.Client?.ServerSettings is { } serverSettings) { - transferAmountInput.MaxValueInt = Math.Min(maxValue, serverSettings.MaximumTransferRequest); - if (serverSettings.MaximumTransferRequest <= 0) + transferAmountInput.MaxValueInt = Math.Min(maxValue, serverSettings.MaximumMoneyTransferRequest); + if (serverSettings.MaximumMoneyTransferRequest <= 0) { transferAmountInput.Enabled = false; transferAmountInput.ToolTip = TextManager.Get("wallettransferrequestdisabled"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs index b7ef43d9f..2f9e0a46f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs @@ -189,6 +189,9 @@ namespace Barotrauma.Tutorials captain_mechanic.CanSpeak = captain_security.CanSpeak = captain_engineer.CanSpeak = false; captain_mechanic.AIController.Enabled = captain_security.AIController.Enabled = captain_engineer.AIController.Enabled = false; + + GameAnalyticsManager.AddDesignEvent("Tutorial:CaptainTutorial:Started"); + GameAnalyticsManager.AddDesignEvent("Tutorial:Started"); } public override IEnumerable UpdateState() @@ -223,6 +226,7 @@ namespace Barotrauma.Tutorials while (!HasOrder(captain_medic, "follow")); SetDoorAccess(tutorial_submarineDoor, tutorial_submarineDoorLight, true); RemoveCompletedObjective(0); + GameAnalyticsManager.AddDesignEvent("Tutorial:CaptainTutorial:Objective0"); // Submarine do { yield return null; } while (!captain_enteredSubmarineSensor.MotionDetected); @@ -238,6 +242,8 @@ namespace Barotrauma.Tutorials //HighlightOrderOption("jobspecific"); } while (!HasOrder(captain_mechanic, "repairsystems") && !HasOrder(captain_mechanic, "repairmechanical") && !HasOrder(captain_mechanic, "repairelectrical")); RemoveCompletedObjective(1); + GameAnalyticsManager.AddDesignEvent("Tutorial:CaptainTutorial:Objective1"); + yield return new WaitForSeconds(2f, false); TriggerTutorialSegment(2, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Command)); GameMain.GameSession.CrewManager.AddCharacter(captain_security); @@ -250,6 +256,8 @@ namespace Barotrauma.Tutorials } while (!HasOrder(captain_security, "operateweapons")); RemoveCompletedObjective(2); + GameAnalyticsManager.AddDesignEvent("Tutorial:CaptainTutorial:Objective2"); + yield return new WaitForSeconds(4f, false); TriggerTutorialSegment(3, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Command)); GameMain.GameSession.CrewManager.AddCharacter(captain_engineer); @@ -265,6 +273,8 @@ namespace Barotrauma.Tutorials } while (!HasOrder(captain_engineer, "operatereactor", "powerup")); RemoveCompletedObjective(3); + GameAnalyticsManager.AddDesignEvent("Tutorial:CaptainTutorial:Objective3"); + do { yield return null; } while (!tutorial_submarineReactor.IsActive); // Wait until reactor on TriggerTutorialSegment(4); while (ContentRunning) yield return null; @@ -279,6 +289,8 @@ namespace Barotrauma.Tutorials } while (Submarine.MainSub.DockedTo.Any()); captain_navConsole.UseAutoDocking = false; RemoveCompletedObjective(4); + GameAnalyticsManager.AddDesignEvent("Tutorial:CaptainTutorial:Objective4"); + yield return new WaitForSeconds(2f, false); TriggerTutorialSegment(5); // Navigate to destination do @@ -294,6 +306,8 @@ namespace Barotrauma.Tutorials } while (captain_sonar.CurrentMode != Sonar.Mode.Active); do { yield return null; } while (Vector2.Distance(Submarine.MainSub.WorldPosition, Level.Loaded.EndPosition) > 4000f); RemoveCompletedObjective(5); + GameAnalyticsManager.AddDesignEvent("Tutorial:CaptainTutorial:Objective5"); + captain_navConsole.UseAutoDocking = true; yield return new WaitForSeconds(4f, false); TriggerTutorialSegment(6); // Docking @@ -303,13 +317,16 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(1.0f, false); } while (!Submarine.MainSub.AtEndExit || !Submarine.MainSub.DockedTo.Any()); RemoveCompletedObjective(6); + GameAnalyticsManager.AddDesignEvent("Tutorial:CaptainTutorial:Objective6"); + yield return new WaitForSeconds(3f, false); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.GetWithVariable("Captain.Radio.Complete", "[OUTPOSTNAME]", GameMain.GameSession.EndLocation.Name), ChatMessageType.Radio, null); SetHighlight(captain_navConsole.Item, false); SetHighlight(captain_sonar.Item, false); SetHighlight(captain_statusMonitor, false); captain.RemoveActiveObjectiveEntity(captain_navConsole.Item); - + + GameAnalyticsManager.AddDesignEvent("Tutorial:CaptainTutorial:Completed"); CoroutineManager.StartCoroutine(TutorialCompleted()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs index 5edeeb26c..4cf46e5f1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs @@ -198,6 +198,9 @@ namespace Barotrauma.Tutorials Item reactorItem = Item.ItemList.Find(i => i.Submarine == Submarine.MainSub && i.GetComponent() != null); reactorItem.GetComponent().AutoTemp = true; + + GameAnalyticsManager.AddDesignEvent("Tutorial:DoctorTutorial:Started"); + GameAnalyticsManager.AddDesignEvent("Tutorial:Started"); } public override IEnumerable UpdateState() @@ -281,6 +284,7 @@ namespace Barotrauma.Tutorials SetHighlight(doctor_suppliesCabinet.Item, false); RemoveCompletedObjective(0); + GameAnalyticsManager.AddDesignEvent("Tutorial:DoctorTutorial:Objective0"); yield return new WaitForSeconds(1.0f, false); @@ -294,6 +298,7 @@ namespace Barotrauma.Tutorials } yield return null; RemoveCompletedObjective(1); + GameAnalyticsManager.AddDesignEvent("Tutorial:DoctorTutorial:Objective1"); yield return new WaitForSeconds(1.0f, false); TriggerTutorialSegment(2); //Treat self while (doctor.CharacterHealth.GetAfflictionStrength("damage") > 0.01f) @@ -311,6 +316,7 @@ namespace Barotrauma.Tutorials } RemoveCompletedObjective(2); + GameAnalyticsManager.AddDesignEvent("Tutorial:DoctorTutorial:Objective2"); SetDoorAccess(doctor_firstDoor, doctor_firstDoorLight, true); while (CharacterHealth.OpenHealthWindow != null) @@ -358,6 +364,7 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(1.0f, false); } RemoveCompletedObjective(3); + GameAnalyticsManager.AddDesignEvent("Tutorial:DoctorTutorial:Objective3"); SetHighlight(doctor_medBayCabinet.Item, true); SetDoorAccess(doctor_thirdDoor, doctor_thirdDoorLight, true); patient1.CharacterHealth.UseHealthWindow = true; @@ -401,6 +408,7 @@ namespace Barotrauma.Tutorials } RemoveCompletedObjective(4); + GameAnalyticsManager.AddDesignEvent("Tutorial:DoctorTutorial:Objective4"); SetHighlight(patient1, false); yield return new WaitForSeconds(1.0f, false); @@ -442,6 +450,7 @@ namespace Barotrauma.Tutorials yield return null; } RemoveCompletedObjective(5); + GameAnalyticsManager.AddDesignEvent("Tutorial:DoctorTutorial:Objective5"); SetHighlight(patient2, false); doctor.RemoveActiveObjectiveEntity(patient2); CoroutineManager.StopCoroutines("KeepPatient2Alive"); @@ -497,6 +506,7 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(1.0f, false); } RemoveCompletedObjective(6); + GameAnalyticsManager.AddDesignEvent("Tutorial:DoctorTutorial:Objective6"); foreach (var patient in subPatients) { SetHighlight(patient, false); @@ -504,6 +514,7 @@ namespace Barotrauma.Tutorials } // END TUTORIAL + GameAnalyticsManager.AddDesignEvent("Tutorial:DoctorTutorial:Completed"); CoroutineManager.StartCoroutine(TutorialCompleted()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs index ca52c58ba..52592248d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs @@ -244,6 +244,9 @@ namespace Barotrauma.Tutorials engineer_submarineJunctionBox_2.Condition = 0f; engineer_submarineJunctionBox_3.Indestructible = false; engineer_submarineJunctionBox_3.Condition = 0f; + + GameAnalyticsManager.AddDesignEvent("Tutorial:EngineerTutorial:Started"); + GameAnalyticsManager.AddDesignEvent("Tutorial:Started"); } public override IEnumerable UpdateState() @@ -317,6 +320,7 @@ namespace Barotrauma.Tutorials yield return null; } while (!engineer_equipmentCabinet.Inventory.IsEmpty()); // Wait until looted RemoveCompletedObjective(0); + GameAnalyticsManager.AddDesignEvent("Tutorial:EngineerTutorial:Objective0"); SetHighlight(engineer_equipmentCabinet.Item, false); SetHighlight(engineer_reactor.Item, true); SetDoorAccess(engineer_firstDoor, engineer_firstDoorLight, true); @@ -352,6 +356,7 @@ namespace Barotrauma.Tutorials yield return null; } while (engineer_reactor.AvailableFuel == 0); RemoveCompletedObjective(1); + GameAnalyticsManager.AddDesignEvent("Tutorial:EngineerTutorial:Objective1"); TriggerTutorialSegment(2); CoroutineManager.StartCoroutine(ReactorOperatedProperly()); do @@ -395,6 +400,7 @@ namespace Barotrauma.Tutorials engineer.SelectedConstruction = null; engineer_reactor.CanBeSelected = false; RemoveCompletedObjective(2); + GameAnalyticsManager.AddDesignEvent("Tutorial:EngineerTutorial:Objective2"); SetHighlight(engineer_reactor.Item, false); SetHighlight(engineer_brokenJunctionBox, true); SetDoorAccess(engineer_secondDoor, engineer_secondDoorLight, true); @@ -421,6 +427,7 @@ namespace Barotrauma.Tutorials } while (repairableJunctionBoxComponent.IsBelowRepairThreshold); // Wait until repaired SetHighlight(engineer_brokenJunctionBox, false); RemoveCompletedObjective(3); + GameAnalyticsManager.AddDesignEvent("Tutorial:EngineerTutorial:Objective3"); SetDoorAccess(engineer_thirdDoor, engineer_thirdDoorLight, true); for (int i = 0; i < engineer_disconnectedJunctionBoxes.Length; i++) { @@ -439,6 +446,7 @@ namespace Barotrauma.Tutorials SetHighlight(engineer_disconnectedJunctionBoxes[i].Item, false); } RemoveCompletedObjective(4); + GameAnalyticsManager.AddDesignEvent("Tutorial:EngineerTutorial:Objective4"); do { yield return null; } while (engineer_workingPump.Item.CurrentHull.WaterPercentage > waterVolumeBeforeOpening); // Wait until drained wiringActive = false; SetDoorAccess(engineer_fourthDoor, engineer_fourthDoorLight, true); @@ -465,6 +473,7 @@ namespace Barotrauma.Tutorials do { CheckJunctionBoxHighlights(repairableJunctionBoxComponent1, repairableJunctionBoxComponent2, repairableJunctionBoxComponent3); yield return null; } while (repairableJunctionBoxComponent1.IsBelowRepairThreshold || repairableJunctionBoxComponent2.IsBelowRepairThreshold || repairableJunctionBoxComponent3.IsBelowRepairThreshold); CheckJunctionBoxHighlights(repairableJunctionBoxComponent1, repairableJunctionBoxComponent2, repairableJunctionBoxComponent3); RemoveCompletedObjective(5); + GameAnalyticsManager.AddDesignEvent("Tutorial:EngineerTutorial:Objective5"); yield return new WaitForSeconds(2f, false); TriggerTutorialSegment(6); // Powerup reactor @@ -474,10 +483,12 @@ namespace Barotrauma.Tutorials engineer.RemoveActiveObjectiveEntity(engineer_submarineReactor.Item); SetHighlight(engineer_submarineReactor.Item, false); RemoveCompletedObjective(6); + GameAnalyticsManager.AddDesignEvent("Tutorial:EngineerTutorial:Objective6"); GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Engineer.Radio.Complete"), ChatMessageType.Radio, null); yield return new WaitForSeconds(4f, false); + GameAnalyticsManager.AddDesignEvent("Tutorial:EngineerTutorial:Completed"); CoroutineManager.StartCoroutine(TutorialCompleted()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs index de6a066f2..71fd7e344 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs @@ -290,6 +290,9 @@ namespace Barotrauma.Tutorials mechanic_ballastPump_2 = Item.ItemList.Find(i => i.HasTag("mechanic_ballastpump_2")).GetComponent(); mechanic_ballastPump_2.Item.Indestructible = false; mechanic_ballastPump_2.Item.Condition = 0f; + + GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Started"); + GameAnalyticsManager.AddDesignEvent("Tutorial:Started"); } public override void Update(float deltaTime) @@ -325,6 +328,7 @@ namespace Barotrauma.Tutorials SetHighlight(mechanic_firstDoor.Item, false); yield return new WaitForSeconds(1.5f, false); RemoveCompletedObjective(0); + GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective0"); // Room 2 yield return new WaitForSeconds(0.0f, false); @@ -368,6 +372,7 @@ namespace Barotrauma.Tutorials SetHighlight(mechanic_equipmentCabinet.Item, false); yield return new WaitForSeconds(1.5f, false); RemoveCompletedObjective(1); + GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective1"); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.Breach"), ChatMessageType.Radio, null); // Room 3 @@ -391,6 +396,8 @@ namespace Barotrauma.Tutorials do { yield return null; } while (WallHasDamagedSections(mechanic_brokenWall_1)); // Highlight until repaired mechanic.RemoveActiveObjectiveEntity(mechanic_brokenWall_1); RemoveCompletedObjective(2); + GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective2"); + yield return new WaitForSeconds(1f, false); TriggerTutorialSegment(3, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select)); // Pump objective SetHighlight(mechanic_workingPump.Item, true); @@ -408,6 +415,8 @@ namespace Barotrauma.Tutorials SetHighlight(mechanic_workingPump.Item, false); do { yield return null; } while (mechanic_brokenhull_1.WaterPercentage > waterVolumeBeforeOpening); // Unlock door once drained RemoveCompletedObjective(3); + GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective3"); + SetDoorAccess(mechanic_thirdDoor, mechanic_thirdDoorLight, true); //TriggerTutorialSegment(11, GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Select], GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Up], GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Down], GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Select]); // Ladder objective //do { yield return null; } while (!mechanic_ladderSensor.MotionDetected); @@ -516,6 +525,8 @@ namespace Barotrauma.Tutorials SetHighlight(mechanic_deconstructor.Item, false); RemoveCompletedObjective(4); + GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective4"); + yield return new WaitForSeconds(1f, false); TriggerTutorialSegment(5); // Fabricate SetHighlight(mechanic_fabricator.Item, true); @@ -565,6 +576,7 @@ namespace Barotrauma.Tutorials yield return null; } while (mechanic.Inventory.FindItemByIdentifier("extinguisher".ToIdentifier()) == null); // Wait until extinguisher is created RemoveCompletedObjective(5); + GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective5"); SetHighlight(mechanic_fabricator.Item, false); SetDoorAccess(mechanic_fourthDoor, mechanic_fourthDoorLight, true); @@ -574,6 +586,7 @@ namespace Barotrauma.Tutorials do { yield return null; } while (!mechanic_fire.Removed); // Wait until extinguished yield return new WaitForSeconds(3f, false); RemoveCompletedObjective(6); + GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective6"); if (mechanic.HasEquippedItem("extinguisher".ToIdentifier())) // do not trigger if dropped already { @@ -584,6 +597,7 @@ namespace Barotrauma.Tutorials yield return null; } while (mechanic.HasEquippedItem("extinguisher".ToIdentifier())); RemoveCompletedObjective(7); + GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective7"); } SetDoorAccess(mechanic_fifthDoor, mechanic_fifthDoorLight, true); @@ -608,6 +622,7 @@ namespace Barotrauma.Tutorials } while (!mechanic.HasEquippedItem("divingsuit".ToIdentifier(), slotType: InvSlotType.OuterClothes)); SetHighlight(mechanic_divingSuitContainer.Item, false); RemoveCompletedObjective(8); + GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective8"); SetDoorAccess(tutorial_mechanicFinalDoor, tutorial_mechanicFinalDoorLight, true); // Room 7 @@ -650,6 +665,7 @@ namespace Barotrauma.Tutorials } } while (repairablePumpComponent.IsBelowRepairThreshold || mechanic_brokenPump.FlowPercentage >= 0 || !mechanic_brokenPump.IsActive); RemoveCompletedObjective(9); + GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective9"); SetHighlight(mechanic_brokenPump.Item, false); do { yield return null; } while (mechanic_brokenhull_2.WaterPercentage > waterVolumeBeforeOpening); SetDoorAccess(tutorial_submarineDoor, tutorial_submarineDoorLight, true); @@ -674,9 +690,11 @@ namespace Barotrauma.Tutorials do { CheckHighlights(repairablePumpComponent1, repairablePumpComponent2, repairableEngineComponent); yield return null; } while (repairablePumpComponent1.IsBelowRepairThreshold || repairablePumpComponent2.IsBelowRepairThreshold || repairableEngineComponent.IsBelowRepairThreshold); CheckHighlights(repairablePumpComponent1, repairablePumpComponent2, repairableEngineComponent); RemoveCompletedObjective(10); + GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective10"); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.Complete"), ChatMessageType.Radio, null); // END TUTORIAL + GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Completed"); CoroutineManager.StartCoroutine(TutorialCompleted()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs index 751cc4cd8..b2514450f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs @@ -251,6 +251,9 @@ namespace Barotrauma.Tutorials officer_subSuperCapacitor_2 = Item.ItemList.Find(i => i.HasTag("officer_subsupercapacitor_2")).GetComponent(); officer_subAmmoShelf = Item.ItemList.Find(i => i.HasTag("officer_subammoshelf")).GetComponent(); SetDoorAccess(tutorial_submarineDoor, tutorial_submarineDoorLight, true); + + GameAnalyticsManager.AddDesignEvent("Tutorial:OfficerTutorial:Started"); + GameAnalyticsManager.AddDesignEvent("Tutorial:Started"); } public override IEnumerable UpdateState() @@ -310,6 +313,7 @@ namespace Barotrauma.Tutorials yield return null; } while (!officer_equipmentCabinet.Inventory.IsEmpty()); // Wait until looted //RemoveCompletedObjective(segments[0]); + GameAnalyticsManager.AddDesignEvent("Tutorial:OfficerTutorial:Objective0"); SetHighlight(officer_equipmentCabinet.Item, false); do { yield return null; } while (IsSelectedItem(officer_equipmentCabinet.Item)); TriggerTutorialSegment(1, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Aim), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot)); // Equip melee weapon & armor @@ -330,6 +334,7 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(1f, false); } while (!officer.HasEquippedItem("stunbaton".ToIdentifier()) || !officer.HasEquippedItem("bodyarmor".ToIdentifier()) || !officer.HasEquippedItem("ballistichelmet1".ToIdentifier())); RemoveCompletedObjective(1); + GameAnalyticsManager.AddDesignEvent("Tutorial:OfficerTutorial:Objective1"); SetDoorAccess(officer_firstDoor, officer_firstDoorLight, true); // Room 3 @@ -338,6 +343,7 @@ namespace Barotrauma.Tutorials officer_crawler = SpawnMonster("crawler", officer_crawlerSpawnPos); do { yield return null; } while (!officer_crawler.IsDead); RemoveCompletedObjective(2); + GameAnalyticsManager.AddDesignEvent("Tutorial:OfficerTutorial:Objective2"); Heal(officer); yield return new WaitForSeconds(1f, false); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Officer.Radio.CrawlerDead"), ChatMessageType.Radio, null); @@ -366,6 +372,8 @@ namespace Barotrauma.Tutorials SetHighlight(officer_ammoShelf_1.Item, false); SetHighlight(officer_ammoShelf_2.Item, false); RemoveCompletedObjective(3); + GameAnalyticsManager.AddDesignEvent("Tutorial:OfficerTutorial:Objective3"); + yield return new WaitForSeconds(2f, false); TriggerTutorialSegment(4, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect)); // Kill hammerhead officer_hammerhead = SpawnMonster("hammerhead", officer_hammerheadSpawnPos); @@ -401,6 +409,8 @@ namespace Barotrauma.Tutorials Heal(officer); SetHighlight(officer_coilgunPeriscope, false); RemoveCompletedObjective(4); + GameAnalyticsManager.AddDesignEvent("Tutorial:OfficerTutorial:Objective4"); + yield return new WaitForSeconds(1f, false); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Officer.Radio.HammerheadDead"), ChatMessageType.Radio, null); SetDoorAccess(officer_thirdDoor, officer_thirdDoorLight, true); @@ -451,6 +461,7 @@ namespace Barotrauma.Tutorials yield return null; } while (!shotGunChamber.Inventory.IsFull(takeStacksIntoAccount: true)); // Wait until all six harpoons loaded RemoveCompletedObjective(5); + GameAnalyticsManager.AddDesignEvent("Tutorial:OfficerTutorial:Objective5"); SetHighlight(officer_rangedWeaponCabinet.Item, false); SetDoorAccess(officer_fourthDoor, officer_fourthDoorLight, true); @@ -461,6 +472,7 @@ namespace Barotrauma.Tutorials do { yield return null; } while (!officer_mudraptor.IsDead); Heal(officer); RemoveCompletedObjective(6); + GameAnalyticsManager.AddDesignEvent("Tutorial:OfficerTutorial:Objective6"); SetDoorAccess(tutorial_securityFinalDoor, tutorial_securityFinalDoorLight, true); // Submarine @@ -512,9 +524,11 @@ namespace Barotrauma.Tutorials officer.RemoveActiveObjectiveEntity(officer_subAmmoBox_1); officer.RemoveActiveObjectiveEntity(officer_subAmmoBox_2); RemoveCompletedObjective(7); + GameAnalyticsManager.AddDesignEvent("Tutorial:OfficerTutorial:Objective7"); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Officer.Radio.Complete"), ChatMessageType.Radio, null); yield return new WaitForSeconds(4f, false); + GameAnalyticsManager.AddDesignEvent("Tutorial:OfficerTutorial:Completed"); CoroutineManager.StartCoroutine(TutorialCompleted()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs index 5c9061d8e..01142cabe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs @@ -252,6 +252,8 @@ namespace Barotrauma.Tutorials Character.Controlled = character = null; Stop(); + GameAnalyticsManager.AddDesignEvent("Tutorial:Died"); + yield return new WaitForSeconds(3.0f); var messageBox = new GUIMessageBox(TextManager.Get("Tutorial.TryAgainHeader"), TextManager.Get("Tutorial.TryAgain"), new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); @@ -273,6 +275,8 @@ namespace Barotrauma.Tutorials Character.Controlled.ClearInputs(); Character.Controlled = null; + GameAnalyticsManager.AddDesignEvent("Tutorial:Completed"); + yield return new WaitForSeconds(waitBeforeFade); var endCinematic = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, Alignment.Center, panDuration: fadeOutTime); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 858971e33..32fae9689 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -1519,6 +1519,7 @@ namespace Barotrauma.Networking serverSettings.LockAllDefaultWires = inc.ReadBoolean(); serverSettings.AllowRagdollButton = inc.ReadBoolean(); serverSettings.AllowLinkingWifiToChat = inc.ReadBoolean(); + serverSettings.MaximumMoneyTransferRequest = inc.ReadInt32(); bool usingShuttle = GameMain.NetLobbyScreen.UsingShuttle = inc.ReadBoolean(); GameMain.LightManager.LosMode = (LosMode)inc.ReadByte(); bool includesFinalize = inc.ReadBoolean(); inc.ReadPadBits(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index 13f093216..326c6f9a0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -635,7 +635,7 @@ namespace Barotrauma.Networking GetPropertyData(nameof(TraitorsMinPlayerCount)).AssignGUIComponent(traitorsMinPlayerCount); var maximumTransferAmount = CreateLabeledNumberInput(roundsTab, "serversettingsmaximumtransferrequest", 0, CampaignMode.MaxMoney, "serversettingsmaximumtransferrequesttooltip"); - GetPropertyData(nameof(MaximumTransferRequest)).AssignGUIComponent(maximumTransferAmount); + GetPropertyData(nameof(MaximumMoneyTransferRequest)).AssignGUIComponent(maximumTransferAmount); var lootedMoneyDestination = CreateLabeledDropdown(roundsTab, "serversettingslootedmoneydestination", numElements: 2, "serversettingslootedmoneydestinationtooltip"); lootedMoneyDestination.AddItem(TextManager.Get("lootedmoneydestination.bank"), LootedMoneyDestination.Bank); diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index a2ee9a9e0..6b590be38 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.0.0 + 0.18.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj.bak b/Barotrauma/BarotraumaClient/LinuxClient.csproj.bak new file mode 100644 index 000000000..a2ee9a9e0 --- /dev/null +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj.bak @@ -0,0 +1,208 @@ + + + + WinExe + netcoreapp3.1 + Barotrauma + FakeFish, Undertow Games + Barotrauma + 0.18.0.0 + Copyright © FakeFish 2018-2022 + AnyCPU;x64 + Barotrauma + ..\BarotraumaShared\Icon.ico + Debug;Release;Unstable + ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + + + + + + DEBUG;TRACE;CLIENT;LINUX;USE_STEAM + x64 + ..\bin\$(Configuration)Linux\ + net6.0 + 8 + + + + TRACE;DEBUG;CLIENT;LINUX;X64;USE_STEAM + x64 + ..\bin\$(Configuration)Linux\ + net6.0 + 8 + + + + TRACE;CLIENT;LINUX;USE_STEAM + x64 + ..\bin\$(Configuration)Linux\ + + + + TRACE;CLIENT;LINUX;USE_STEAM;UNSTABLE + x64 + ..\bin\$(Configuration)Linux\ + true + + + + TRACE;CLIENT;LINUX;X64;USE_STEAM + x64 + ..\bin\$(Configuration)Linux\ + + + + TRACE;CLIENT;LINUX;X64;USE_STEAM;UNSTABLE + x64 + ..\bin\$(Configuration)Linux\ + true + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + PreserveNewest + + + + Icon.bmp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(IntermediateOutputPath)gitver + $(IntermediateOutputPath)gitbranch + + + + + + + + + + + + + + + + + + @(GitVersion) + + + + + + + + + @(GitBranch) + + + + + + + $(IntermediateOutputPath)CustomAssemblyInfo.cs + + + + + + + + + <_Parameter1>GitRevision + <_Parameter2>$(BuildHash) + + + <_Parameter1>GitBranch + <_Parameter2>$(BuildBranch) + + + <_Parameter1>ProjectDir + <_Parameter2>$(ProjectDir) + + + + + + + + linux-x64 + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 9360c73bf..313a64a2f 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.0.0 + 0.18.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj.bak b/Barotrauma/BarotraumaClient/MacClient.csproj.bak new file mode 100644 index 000000000..9360c73bf --- /dev/null +++ b/Barotrauma/BarotraumaClient/MacClient.csproj.bak @@ -0,0 +1,214 @@ + + + + WinExe + netcoreapp3.1 + Barotrauma + FakeFish, Undertow Games + Barotrauma + 0.18.0.0 + Copyright © FakeFish 2018-2022 + AnyCPU;x64 + Barotrauma + ..\BarotraumaShared\Icon.ico + Debug;Release;Unstable + ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + + + + TRACE;CLIENT;OSX;USE_STEAM;DEBUG;NETCOREAPP;NETCOREAPP3_0 + x64 + ..\bin\$(Configuration)Mac + + + + TRACE;DEBUG;CLIENT;OSX;X64;USE_STEAM + x64 + ..\bin\$(Configuration)Mac\ + + + + TRACE;CLIENT;OSX;USE_STEAM;RELEASE;NETCOREAPP;NETCOREAPP3_0 + x64 + + ..\bin\$(Configuration)Mac + + + + TRACE;CLIENT;OSX;USE_STEAM;RELEASE;NETCOREAPP;NETCOREAPP3_0;UNSTABLE + x64 + + ..\bin\$(Configuration)Mac + true + + + + TRACE;CLIENT;OSX;X64;USE_STEAM + x64 + ..\bin\$(Configuration)Mac\ + + + + TRACE;CLIENT;OSX;X64;USE_STEAM;UNSTABLE + x64 + ..\bin\$(Configuration)Mac\ + true + + + + + + + + + + + SharedSource\Prefabs\PrefabSelector.cs + + + SharedSource\Prefabs\PrefabCollectionSubset.cs + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + Icon.bmp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + libsteam_api64.dylib + PreserveNewest + + + + + PreserveNewest + + + + + + $(IntermediateOutputPath)gitver + $(IntermediateOutputPath)gitbranch + + + + + + + + + + + + + + + + + + @(GitVersion) + + + + + + + + + @(GitBranch) + + + + + + + $(IntermediateOutputPath)CustomAssemblyInfo.cs + + + + + + + + + <_Parameter1>GitRevision + <_Parameter2>$(BuildHash) + + + <_Parameter1>GitBranch + <_Parameter2>$(BuildBranch) + + + <_Parameter1>ProjectDir + <_Parameter2>$(ProjectDir) + + + + + + + + osx-x64 + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index b9fecd89c..b17eb41f9 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.0.0 + 0.18.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj.bak b/Barotrauma/BarotraumaClient/WindowsClient.csproj.bak new file mode 100644 index 000000000..b9fecd89c --- /dev/null +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj.bak @@ -0,0 +1,237 @@ + + + + WinExe + netcoreapp3.1 + Barotrauma + FakeFish, Undertow Games + Barotrauma + 0.18.0.0 + Copyright © FakeFish 2018-2022 + AnyCPU;x64 + Barotrauma + ..\BarotraumaShared\Icon.ico + Debug;Release;Unstable + app.manifest + ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + + + + DEBUG;TRACE;CLIENT;WINDOWS;USE_STEAM + x64 + ..\bin\$(Configuration)Windows\ + + + + TRACE;DEBUG;CLIENT;WINDOWS;X64;USE_STEAM + x64 + ..\bin\$(Configuration)Windows\ + full + true + Auto + + + + TRACE;CLIENT;WINDOWS;USE_STEAM + x64 + ..\bin\$(Configuration)Windows\ + + + + TRACE;CLIENT;WINDOWS;USE_STEAM + x64 + ..\bin\$(Configuration)Windows\ + true + + + + TRACE;CLIENT;WINDOWS;X64;USE_STEAM + x64 + ..\bin\$(Configuration)Windows\ + full + true + + + + TRACE;CLIENT;WINDOWS;X64;USE_STEAM + x64 + ..\bin\$(Configuration)Windows\ + full + true + true + + + + + + + + + SharedSource\Steam\AuthTicket.cs + + + SharedSource\Utils\Result.cs + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + + + + SharedSource\Utils\Result + + + + + + + + + + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + + + + + + $(IntermediateOutputPath)gitver + $(IntermediateOutputPath)gitbranch + + + + + + + + + + + + + + + + + + @(GitVersion) + + + + + + + + + @(GitBranch) + + + + + + + $(IntermediateOutputPath)CustomAssemblyInfo.cs + + + + + + + + + <_Parameter1>GitRevision + <_Parameter2>$(BuildHash) + + + <_Parameter1>GitBranch + <_Parameter2>$(BuildBranch) + + + <_Parameter1>ProjectDir + <_Parameter2>$(ProjectDir) + + + + + + + + win-x64 + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 0f61a36a8..2eedc6301 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.0.0 + 0.18.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj.bak b/Barotrauma/BarotraumaServer/LinuxServer.csproj.bak new file mode 100644 index 000000000..ab1e78f08 --- /dev/null +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj.bak @@ -0,0 +1,156 @@ + + + + Exe + netcoreapp3.1 + Barotrauma + FakeFish, Undertow Games + Barotrauma Dedicated Server + 0.18.0.0 + Copyright © FakeFish 2018-2022 + AnyCPU;x64 + DedicatedServer + ..\BarotraumaShared\Icon.ico + Debug;Release;Unstable + ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + + + + + + DEBUG;TRACE;SERVER;LINUX;USE_STEAM + x64 + ..\bin\$(Configuration)Linux\ + net6.0 + 8 + + + + TRACE;DEBUG;SERVER;LINUX;X64;USE_STEAM + x64 + ..\bin\$(Configuration)Linux\ + net6.0 + 8 + + + + TRACE;SERVER;LINUX;USE_STEAM + x64 + ..\bin\$(Configuration)Linux\ + + + + TRACE;SERVER;LINUX;USE_STEAM + x64 + ..\bin\$(Configuration)Linux\ + true + + + + TRACE;SERVER;LINUX;X64;USE_STEAM + x64 + ..\bin\$(Configuration)Linux\ + + + + TRACE;SERVER;LINUX;X64;USE_STEAM + x64 + ..\bin\$(Configuration)Linux\ + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(IntermediateOutputPath)gitver + $(IntermediateOutputPath)gitbranch + + + + + + + + + + + + + + + + + + @(GitVersion) + + + + + + + + + @(GitBranch) + + + + + + + $(IntermediateOutputPath)CustomAssemblyInfo.cs + + + + + + + + + <_Parameter1>GitRevision + <_Parameter2>$(BuildHash) + + + <_Parameter1>GitBranch + <_Parameter2>$(BuildBranch) + + + <_Parameter1>ProjectDir + <_Parameter2>$(ProjectDir) + + + + + + + diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 75305843b..41800c1ca 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.0.0 + 0.18.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj.bak b/Barotrauma/BarotraumaServer/MacServer.csproj.bak new file mode 100644 index 000000000..105be0054 --- /dev/null +++ b/Barotrauma/BarotraumaServer/MacServer.csproj.bak @@ -0,0 +1,157 @@ + + + + Exe + netcoreapp3.1 + Barotrauma + FakeFish, Undertow Games + Barotrauma Dedicated Server + 0.18.0.0 + Copyright © FakeFish 2018-2022 + AnyCPU;x64 + DedicatedServer + ..\BarotraumaShared\Icon.ico + Debug;Release;Unstable + ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + + + + TRACE;SERVER;OSX;USE_STEAM;DEBUG;NETCOREAPP;NETCOREAPP3_0 + x64 + ..\bin\DebugMac + true + + + + + TRACE;DEBUG;SERVER;OSX;X64;USE_STEAM + x64 + ..\bin\$(Configuration)Mac\ + + + + TRACE;SERVER;OSX;USE_STEAM;RELEASE;NETCOREAPP;NETCOREAPP3_0 + x64 + + ..\bin\ReleaseMac + + + + TRACE;SERVER;OSX;USE_STEAM;RELEASE;NETCOREAPP;NETCOREAPP3_0;UNSTABLE + x64 + + ..\bin\ReleaseMac + true + + + + TRACE;SERVER;OSX;X64;USE_STEAM + x64 + ..\bin\$(Configuration)Mac\ + + + + TRACE;SERVER;OSX;X64;USE_STEAM;UNSTABLE + x64 + ..\bin\$(Configuration)Mac\ + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + libsteam_api64.dylib + PreserveNewest + + + + + + $(IntermediateOutputPath)gitver + $(IntermediateOutputPath)gitbranch + + + + + + + + + + + + + + + + + + @(GitVersion) + + + + + + + + + @(GitBranch) + + + + + + + $(IntermediateOutputPath)CustomAssemblyInfo.cs + + + + + + + + + <_Parameter1>GitRevision + <_Parameter2>$(BuildHash) + + + <_Parameter1>GitBranch + <_Parameter2>$(BuildBranch) + + + <_Parameter1>ProjectDir + <_Parameter2>$(ProjectDir) + + + + + + + diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 858cb5b29..89ed9f0dc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -930,7 +930,7 @@ namespace Barotrauma { if (transfer.Receiver is Some { Value: var receiverId } && receiverId == sender.CharacterID) { - if (transfer.Amount > GameMain.Server.ServerSettings.MaximumTransferRequest) { return; } + if (transfer.Amount > GameMain.Server.ServerSettings.MaximumMoneyTransferRequest) { return; } GameMain.Server.Voting.StartTransferVote(sender, null, transfer.Amount, sender); GameServer.Log($"{sender.Name} started a vote to transfer {transfer.Amount} mk from the bank.", ServerLog.MessageType.Money); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index effeb21a5..8b3b20d36 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -2465,6 +2465,7 @@ namespace Barotrauma.Networking msg.Write(serverSettings.LockAllDefaultWires); msg.Write(serverSettings.AllowRagdollButton); msg.Write(serverSettings.AllowLinkingWifiToChat); + msg.Write(serverSettings.MaximumMoneyTransferRequest); msg.Write(IsUsingRespawnShuttle()); msg.Write((byte)serverSettings.LosMode); msg.Write(includesFinalize); msg.WritePadBits(); diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index c5f7034cc..d83075d47 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.0.0 + 0.18.1.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj.bak b/Barotrauma/BarotraumaServer/WindowsServer.csproj.bak new file mode 100644 index 000000000..0fbb606a7 --- /dev/null +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj.bak @@ -0,0 +1,152 @@ + + + + Exe + netcoreapp3.1 + Barotrauma + FakeFish, Undertow Games + Barotrauma Dedicated Server + 0.18.0.0 + Copyright © FakeFish 2018-2022 + AnyCPU;x64 + DedicatedServer + ..\BarotraumaShared\Icon.ico + Debug;Release;Unstable + ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + + + + DEBUG;TRACE;SERVER;WINDOWS;USE_STEAM + x64 + ..\bin\$(Configuration)Windows\ + + + + TRACE;DEBUG;SERVER;WINDOWS;X64;USE_STEAM + x64 + ..\bin\$(Configuration)Windows\ + full + true + + + + TRACE;SERVER;WINDOWS;USE_STEAM + x64 + ..\bin\$(Configuration)Windows\ + + + + TRACE;SERVER;WINDOWS;USE_STEAM + x64 + ..\bin\$(Configuration)Windows\ + true + + + + TRACE;SERVER;WINDOWS;X64;USE_STEAM + x64 + ..\bin\$(Configuration)Windows\ + full + true + + + + TRACE;SERVER;WINDOWS;X64;USE_STEAM + x64 + ..\bin\$(Configuration)Windows\ + full + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(IntermediateOutputPath)gitver + $(IntermediateOutputPath)gitbranch + + + + + + + + + + + + + + + + + + @(GitVersion) + + + + + + + + + @(GitBranch) + + + + + + + $(IntermediateOutputPath)CustomAssemblyInfo.cs + + + + + + + + + <_Parameter1>GitRevision + <_Parameter2>$(BuildHash) + + + <_Parameter1>GitBranch + <_Parameter2>$(BuildBranch) + + + <_Parameter1>ProjectDir + <_Parameter2>$(ProjectDir) + + + + + + + diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 733238123..a421a0be1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -18,11 +18,14 @@ namespace Barotrauma bool skipMainSubs = GameMain.GameSession.GameMode is CampaignMode { IsFirstRound: false }; if (!skipMainSubs) { + if (Submarine.MainSub is Submarine mainSub && mainSub.Info.IsPlayer) + { + SpawnStartItems(mainSub); + } for (int i = 0; i < Submarine.MainSubs.Length; i++) { var sub = Submarine.MainSubs[i]; if (sub == null || sub.Info.InitialSuppliesSpawned) { continue; } - SpawnStartItems(sub); var subs = sub.GetConnectedSubs().Where(s => s.TeamID == sub.TeamID); CreateAndPlace(subs); subs.ForEach(s => s.Info.InitialSuppliesSpawned = true); @@ -79,7 +82,6 @@ namespace Barotrauma return; } initialSpawnPos = spawnHull; - } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index dd88b19f1..a95f15214 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -388,7 +388,7 @@ namespace Barotrauma.Items.Components IsActive = isActive; } - public void SetRequiredItems(ContentXElement element) + public void SetRequiredItems(ContentXElement element, bool allowEmpty = false) { bool returnEmpty = false; #if CLIENT @@ -410,7 +410,7 @@ namespace Barotrauma.Items.Components requiredItems[ri.Type].Add(ri); } } - else + else if (!allowEmpty) { DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - component " + GetType().ToString() + " requires an item with no identifiers."); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 426a8d9ef..8b1831507 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -301,8 +301,12 @@ namespace Barotrauma /// public enum LevelGenStage { + LevelGenParams, + Size, GenStart, TunnelGen, + AbyssGen, + CaveGen, VoronoiGen, VoronoiGen2, VoronoiGen3, @@ -327,6 +331,11 @@ namespace Barotrauma equalityCheckValues[stage] = Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient); } + private void SetEqualityCheckValue(LevelGenStage stage, int value) + { + equalityCheckValues[stage] = value; + } + private void ClearEqualityCheckValues() { foreach (LevelGenStage stage in Enum.GetValues(typeof(LevelGenStage))) @@ -445,6 +454,9 @@ namespace Barotrauma } GenerateEqualityCheckValue(LevelGenStage.GenStart); + SetEqualityCheckValue(LevelGenStage.LevelGenParams, unchecked((int)GenerationParams.UintIdentifier)); + SetEqualityCheckValue(LevelGenStage.Size, borders.Width ^ borders.Height << 16); + GenerateEqualityCheckValue(LevelGenStage.TunnelGen); LevelObjectManager = new LevelObjectManager(); @@ -582,10 +594,12 @@ namespace Barotrauma } int sideTunnelCount = Rand.Range(GenerationParams.SideTunnelCount.X, GenerationParams.SideTunnelCount.Y + 1, Rand.RandSync.ServerAndClient); + for (int j = 0; j < sideTunnelCount; j++) { if (mainPath.Nodes.Count < 4) { break; } var validTunnels = Tunnels.FindAll(t => t.Type != TunnelType.Cave && t != startPath && t != endPath && t != endHole && t != abyssTunnel); + Tunnel tunnelToBranchOff = validTunnels[Rand.Int(validTunnels.Count, Rand.RandSync.ServerAndClient)]; if (tunnelToBranchOff == null) { tunnelToBranchOff = mainPath; } @@ -600,7 +614,13 @@ namespace Barotrauma CalculateTunnelDistanceField(null); GenerateSeaFloorPositions(); + + GenerateEqualityCheckValue(LevelGenStage.AbyssGen); + GenerateAbyssArea(); + + GenerateEqualityCheckValue(LevelGenStage.CaveGen); + GenerateCaves(mainPath); GenerateEqualityCheckValue(LevelGenStage.VoronoiGen); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index d4e56b2e1..bc380746d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -584,7 +584,9 @@ namespace Barotrauma throw new InvalidOperationException("Level generation presets not found - using default presets"); } - var matchingLevelParams = LevelParams.Where(lp => + var levelParamsOrdered = LevelParams.OrderBy(l => l.UintIdentifier); + + var matchingLevelParams = levelParamsOrdered.Where(lp => lp.Type == type && (lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Any()) && !lp.AllowedBiomeIdentifiers.Contains("None".ToIdentifier())); @@ -598,11 +600,11 @@ namespace Barotrauma if (!biome.IsEmpty) { //try to find params that at least have a suitable type - matchingLevelParams = LevelParams.Where(lp => lp.Type == type); + matchingLevelParams = levelParamsOrdered.Where(lp => lp.Type == type); if (!matchingLevelParams.Any()) { //still not found, give up and choose some params randomly - matchingLevelParams = LevelParams; + matchingLevelParams = levelParamsOrdered; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 10e45816a..b5aae541a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -911,7 +911,7 @@ namespace Barotrauma.Networking public LootedMoneyDestination LootedMoneyDestination { get; set; } [Serialize(999999, IsPropertySaveable.Yes)] - public int MaximumTransferRequest { get; set; } + public int MaximumMoneyTransferRequest { get; set; } private int maxMissionCount = CampaignSettings.DefaultMaxMissionCount; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 765a62374..99bb15486 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -1107,7 +1107,7 @@ namespace Barotrauma itemComponent.requiredItems.Clear(); itemComponent.DisabledRequiredItems.Clear(); - itemComponent.SetRequiredItems(element); + itemComponent.SetRequiredItems(element, allowEmpty: true); break; } } diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 2942b8e72..adc356451 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,15 @@ +--------------------------------------------------------------------------------------------------------- +v0.18.1.0 +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Fixed crashing when someone respawns while the tab menu is open. +- Fixed level equality errors in multiplayer game modes other than the campaign. +- Fixed headsets from pre-0.18 saves being unusable due to still having the battery requirement but no inventory to hold one. +- Fixed inability to put disposable diving suits in diving suit lockers. +- Fixed clients without money management permissions not being able to request money. +- Fixed context menus rendering behind the debug console. + --------------------------------------------------------------------------------------------------------- v0.18.0.0 --------------------------------------------------------------------------------------------------------- @@ -43,7 +55,7 @@ Changes and additions: - Improved tooltips in the wallet menu to make their function more clear. - Corpses can now be grabbed in singleplayer to loot money. - Made the crew wallet menu update when the players permissions change. -- Prevented selling items from submarine containers tagged with "donttakeitems", instead of "donttakeitems". +- Prevented selling items from submarine containers tagged with "dontsellitems", instead of "donttakeitems". - Removed merchant balance effect on item prices. - Replaced "item sell value" with the location reputation effect on the store interface. From 077917fa5dfb71aec1d225da6c662a46af9dc50e Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Thu, 19 May 2022 23:43:21 +0900 Subject: [PATCH 03/14] Build 0.18.2.0 --- .../BarotraumaClient/ClientSource/Camera.cs | 12 +- .../ClientSource/Characters/CharacterInfo.cs | 5 - .../Events/EventActions/ConversationAction.cs | 4 +- .../BarotraumaClient/ClientSource/GUI/GUI.cs | 96 ++++--- .../ClientSource/GUI/GUINumberInput.cs | 5 - .../ClientSource/GUI/Store.cs | 2 +- .../ClientSource/GUI/TabMenu.cs | 2 +- .../BarotraumaClient/ClientSource/GameMain.cs | 7 +- .../GameModes/SinglePlayerCampaign.cs | 3 +- .../ClientSource/Items/Components/Growable.cs | 6 +- .../Items/Components/ItemContainer.cs | 4 +- .../Items/Components/LightComponent.cs | 6 +- .../Components/Machines/Deconstructor.cs | 2 +- .../Items/Components/Machines/MiniMap.cs | 17 +- .../Items/Components/Machines/Pump.cs | 9 +- .../Items/Components/Repairable.cs | 5 +- .../ClientSource/Items/Components/Rope.cs | 2 + .../Components/Signal/CustomInterface.cs | 129 +++++++--- .../ClientSource/Items/Item.cs | 18 +- .../ClientSource/Map/Levels/LevelRenderer.cs | 12 - .../ClientSource/Map/Levels/WaterRenderer.cs | 9 - .../ClientSource/Map/Lights/LightManager.cs | 128 +++++++--- .../ClientSource/Map/Lights/LightSource.cs | 21 +- .../ClientSource/Map/Map/Map.cs | 2 +- .../ClientSource/Map/SubmarinePreview.cs | 7 +- .../Networking/FileTransfer/FileReceiver.cs | 29 +-- .../ClientSource/Networking/GameClient.cs | 4 +- .../ClientSource/Networking/KarmaManager.cs | 2 +- .../Primitives/Peers/SteamP2PClientPeer.cs | 6 - .../Primitives/Peers/SteamP2POwnerPeer.cs | 6 - .../ClientSource/Networking/ServerSettings.cs | 56 +++-- .../CharacterEditor/CharacterEditorScreen.cs | 2 +- .../Screens/CharacterEditor/Wizard.cs | 8 +- .../ClientSource/Screens/EditorScreen.cs | 6 +- .../Screens/EventEditor/EventEditorScreen.cs | 2 +- .../ClientSource/Screens/LevelEditorScreen.cs | 37 +-- .../ClientSource/Screens/NetLobbyScreen.cs | 1 + .../Screens/SpriteEditorScreen.cs | 2 +- .../ClientSource/Screens/SubEditorScreen.cs | 35 ++- .../Serialization/SerializableEntityEditor.cs | 30 +-- .../ClientSource/Settings/SettingsMenu.cs | 28 ++- .../ClientSource/Sounds/SoundPrefab.cs | 5 - .../ClientSource/Steam/Workshop.cs | 5 - .../Steam/WorkshopMenu/Mutable/PublishTab.cs | 7 +- .../ClientSource/Utils/WikiImage.cs | 2 +- .../BarotraumaClient/LinuxClient.csproj | 11 +- .../BarotraumaClient/LinuxClient.csproj.bak | 208 --------------- Barotrauma/BarotraumaClient/MacClient.csproj | 17 +- .../BarotraumaClient/MacClient.csproj.bak | 214 ---------------- .../BarotraumaClient/WindowsClient.csproj | 19 +- .../BarotraumaClient/WindowsClient.csproj.bak | 237 ------------------ .../BarotraumaServer/LinuxServer.csproj | 5 +- .../BarotraumaServer/LinuxServer.csproj.bak | 156 ------------ Barotrauma/BarotraumaServer/MacServer.csproj | 5 +- .../BarotraumaServer/MacServer.csproj.bak | 157 ------------ .../Components/Signal/CustomInterface.cs | 35 ++- .../ServerSource/Networking/Voting.cs | 3 +- .../BarotraumaServer/WindowsServer.csproj | 5 +- .../BarotraumaServer/WindowsServer.csproj.bak | 152 ----------- .../Data/Saves/TheColdsBelow.save | Bin 0 -> 322104 bytes .../BarotraumaShared/Data/Saves/Zapisz_1.save | Bin 0 -> 240323 bytes .../Characters/AI/EnemyAIController.cs | 6 +- .../Characters/AI/Wreck/WreckAIConfig.cs | 2 +- .../SharedSource/Characters/CharacterInfo.cs | 79 +++++- .../SharedSource/Characters/Limb.cs | 2 +- .../Characters/Params/CharacterParams.cs | 2 +- .../SharedSource/DebugConsole.cs | 2 +- .../BarotraumaShared/SharedSource/Enums.cs | 14 +- .../Events/EventActions/ConversationAction.cs | 10 +- .../Events/Missions/MonsterMission.cs | 2 + .../SharedSource/Events/ScriptedEvent.cs | 4 +- .../GameAnalytics/GameAnalyticsManager.cs | 5 - .../GameSession/AutoItemPlacer.cs | 27 +- .../SharedSource/GameSession/GameSession.cs | 5 - .../Items/Components/DockingPort.cs | 4 +- .../Components/Holdable/LevelResource.cs | 1 - .../Items/Components/Holdable/RangedWeapon.cs | 2 +- .../Items/Components/ItemContainer.cs | 11 +- .../Items/Components/Machines/MiniMap.cs | 4 +- .../Items/Components/Machines/Steering.cs | 23 +- .../Items/Components/Power/Powered.cs | 18 +- .../Items/Components/Projectile.cs | 3 +- .../SharedSource/Items/Components/Quality.cs | 3 +- .../Items/Components/Repairable.cs | 20 +- .../SharedSource/Items/Components/Rope.cs | 3 +- .../Components/Signal/CustomInterface.cs | 68 ++++- .../Items/Components/Signal/WaterDetector.cs | 12 +- .../SharedSource/Items/Components/Turret.cs | 4 +- .../SharedSource/Items/Item.cs | 49 +++- .../SharedSource/Items/RelatedItem.cs | 15 +- .../SharedSource/Items/StartItemSet.cs | 36 +++ .../SharedSource/Map/CoreEntityPrefab.cs | 39 ++- .../BarotraumaShared/SharedSource/Map/Gap.cs | 4 +- .../BarotraumaShared/SharedSource/Map/Hull.cs | 12 +- .../SharedSource/Map/Levels/Biome.cs | 8 +- .../Map/Levels/CaveGenerationParams.cs | 21 +- .../SharedSource/Map/Levels/Level.cs | 16 +- .../SharedSource/Map/Levels/LevelData.cs | 6 +- .../Map/Levels/LevelGenerationParams.cs | 2 +- .../Levels/LevelObjects/LevelObjectManager.cs | 8 +- .../Levels/LevelObjects/LevelObjectPrefab.cs | 18 +- .../SharedSource/Map/Levels/LevelWall.cs | 6 - .../SharedSource/Map/Map/Location.cs | 55 +++- .../SharedSource/Map/Map/Map.cs | 45 +++- .../SharedSource/Map/Outposts/NPCSet.cs | 19 +- .../SharedSource/Map/WayPoint.cs | 17 +- .../Networking/ChildServerRelay.cs | 2 +- .../Prefabs/IImplementsVariants.cs | 6 + .../SharedSource/Settings/GameSettings.cs | 2 + .../SharedSource/Upgrades/Upgrade.cs | 13 +- .../SharedSource/Upgrades/UpgradePrefab.cs | 21 +- .../SharedSource/Utils/NamedEvent.cs | 13 +- .../SharedSource/Utils/SafeIO.cs | 8 +- .../SharedSource/Utils/SaveUtil.cs | 2 +- Barotrauma/BarotraumaShared/changelog.txt | 54 ++++ 115 files changed, 1080 insertions(+), 1763 deletions(-) delete mode 100644 Barotrauma/BarotraumaClient/LinuxClient.csproj.bak delete mode 100644 Barotrauma/BarotraumaClient/MacClient.csproj.bak delete mode 100644 Barotrauma/BarotraumaClient/WindowsClient.csproj.bak delete mode 100644 Barotrauma/BarotraumaServer/LinuxServer.csproj.bak delete mode 100644 Barotrauma/BarotraumaServer/MacServer.csproj.bak delete mode 100644 Barotrauma/BarotraumaServer/WindowsServer.csproj.bak create mode 100644 Barotrauma/BarotraumaShared/Data/Saves/TheColdsBelow.save create mode 100644 Barotrauma/BarotraumaShared/Data/Saves/Zapisz_1.save create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Items/StartItemSet.cs diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index e8d9f99f1..db7c27941 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -6,7 +6,7 @@ using System; namespace Barotrauma { - public class Camera + public class Camera : IDisposable { public static bool FollowSub = true; @@ -147,15 +147,19 @@ namespace Barotrauma position = Vector2.Zero; CreateMatrices(); - // TODO: Needs to unregister if ever destroy cameras. + // 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); } - ~Camera() + private bool disposed = false; + public void Dispose() { - GameMain.Instance.ResolutionChanged -= CreateMatrices; + if (!disposed) { GameMain.Instance.ResolutionChanged -= CreateMatrices; } + disposed = true; } public Vector2 TargetPos { get; set; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index d96f35a87..deca50826 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -988,11 +988,6 @@ namespace Barotrauma HeadSelectionList = null; } } - - ~AppearanceCustomizationMenu() - { - Dispose(); - } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs index 7f8c08485..d9dd3a9cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -308,7 +308,7 @@ namespace Barotrauma AlwaysOverrideCursor = true }; - LocalizedString translatedText = TextManager.Get(text); + LocalizedString translatedText = TextManager.Get(text).Fallback(text); if (speaker?.Info != null && drawChathead) { @@ -335,7 +335,7 @@ namespace Barotrauma { foreach (string option in options) { - var btn = new GUIButton(new RectTransform(new Vector2(0.9f, 0.01f), textContent.RectTransform), TextManager.Get(option), style: "ListBoxElement"); + var btn = new GUIButton(new RectTransform(new Vector2(0.9f, 0.01f), textContent.RectTransform), TextManager.Get(option).Fallback(option), style: "ListBoxElement"); btn.TextBlock.TextAlignment = Alignment.CenterLeft; btn.TextColor = btn.HoverTextColor = GUIStyle.Green; btn.TextBlock.Wrap = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 8d65228ff..ddddc7fc9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -301,6 +301,7 @@ namespace Barotrauma } float startY = 10.0f; + float yStep = AdjustForTextScale(18) * yScale; if (GameMain.ShowFPS || GameMain.DebugDraw || GameMain.ShowPerf) { float y = startY; @@ -309,11 +310,38 @@ namespace Barotrauma Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); if (GameMain.GameSession != null && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 1.0) { - y += AdjustForTextScale(15) * yScale; + y += yStep; DrawString(spriteBatch, new Vector2(10, y), $"Physics: {GameMain.CurrentUpdateRate}", (GameMain.CurrentUpdateRate < Timing.FixedUpdateRate) ? Color.Red : Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); } + if (GameMain.DebugDraw || GameMain.ShowPerf) + { + y += yStep; + DrawString(spriteBatch, new Vector2(10, y), + "Active lights: " + Lights.LightManager.ActiveLightCount, + Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); + y += yStep; + DrawString(spriteBatch, new Vector2(10, y), + "Physics: " + GameMain.World.UpdateTime.TotalMilliseconds + " ms", + Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); + y += yStep; + try + { + DrawString(spriteBatch, new Vector2(10, y), + $"Bodies: {GameMain.World.BodyList.Count} ({GameMain.World.BodyList.Count(b => b != null && b.Awake && b.Enabled)} awake, {GameMain.World.BodyList.Count(b => b != null && b.Awake && b.BodyType == BodyType.Dynamic && b.Enabled)} dynamic)", + Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); + } + catch (InvalidOperationException) + { + DebugConsole.AddWarning("Exception while rendering debug info. Physics bodies may have been created or removed while rendering."); + } + y += yStep; + DrawString(spriteBatch, new Vector2(10, y), + "Particle count: " + GameMain.ParticleManager.ParticleCount + "/" + GameMain.ParticleManager.MaxParticles, + Color.Lerp(GUIStyle.Green, GUIStyle.Red, (GameMain.ParticleManager.ParticleCount / (float)GameMain.ParticleManager.MaxParticles)), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + + } } if (GameMain.ShowPerf) @@ -324,67 +352,59 @@ namespace Barotrauma "Draw - Avg: " + GameMain.PerformanceCounter.DrawTimeGraph.Average().ToString("0.00") + " ms" + " Max: " + GameMain.PerformanceCounter.DrawTimeGraph.LargestValue().ToString("0.00") + " ms", GUIStyle.Green, Color.Black * 0.8f, font: GUIStyle.SmallFont); - y += 15 * yScale; + y += yStep; GameMain.PerformanceCounter.DrawTimeGraph.Draw(spriteBatch, new Rectangle((int)x, (int)y, 170, 50), color: GUIStyle.Green); - y += 50 * yScale; + y += yStep * 3; DrawString(spriteBatch, new Vector2(x, y), "Update - Avg: " + GameMain.PerformanceCounter.UpdateTimeGraph.Average().ToString("0.00") + " ms" + " Max: " + GameMain.PerformanceCounter.UpdateTimeGraph.LargestValue().ToString("0.00") + " ms", Color.LightBlue, Color.Black * 0.8f, font: GUIStyle.SmallFont); - y += 15 * yScale; + y += yStep; GameMain.PerformanceCounter.UpdateTimeGraph.Draw(spriteBatch, new Rectangle((int)x, (int)y, 170, 50), color: Color.LightBlue); - y += 50 * yScale; + y += yStep * 3; foreach (string key in GameMain.PerformanceCounter.GetSavedIdentifiers) { float elapsedMillisecs = GameMain.PerformanceCounter.GetAverageElapsedMillisecs(key); DrawString(spriteBatch, new Vector2(x, y), key + ": " + elapsedMillisecs.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, elapsedMillisecs / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - y += 15 * yScale; + y += yStep; foreach (string childKey in GameMain.PerformanceCounter.GetSavedPartialIdentifiers(key)) { elapsedMillisecs = GameMain.PerformanceCounter.GetPartialAverageElapsedMillisecs(key, childKey); DrawString(spriteBatch, new Vector2(x + 15, y), childKey + ": " + elapsedMillisecs.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, elapsedMillisecs / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - y += 15 * yScale; + y += yStep; } } if (Powered.Grids != null) { DrawString(spriteBatch, new Vector2(x, y), "Grids: " + Powered.Grids.Count, Color.LightGreen, Color.Black * 0.5f, 0, GUIStyle.SmallFont); - y += 15 * yScale; + y += yStep; } if (Settings.EnableDiagnostics) { - x += 20 * xScale; + x += yStep * 2; DrawString(spriteBatch, new Vector2(x, y), "ContinuousPhysicsTime: " + GameMain.World.ContinuousPhysicsTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ContinuousPhysicsTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - DrawString(spriteBatch, new Vector2(x, y + 15 * yScale), "ControllersUpdateTime: " + GameMain.World.ControllersUpdateTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ControllersUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - DrawString(spriteBatch, new Vector2(x, y + 30 * yScale), "AddRemoveTime: " + GameMain.World.AddRemoveTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.AddRemoveTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - DrawString(spriteBatch, new Vector2(x, y + 45 * yScale), "NewContactsTime: " + GameMain.World.NewContactsTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.NewContactsTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - DrawString(spriteBatch, new Vector2(x, y + 60 * yScale), "ContactsUpdateTime: " + GameMain.World.ContactsUpdateTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ContactsUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - DrawString(spriteBatch, new Vector2(x, y + 75 * yScale), "SolveUpdateTime: " + GameMain.World.SolveUpdateTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.SolveUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + DrawString(spriteBatch, new Vector2(x, y + yStep), "ControllersUpdateTime: " + GameMain.World.ControllersUpdateTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ControllersUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + DrawString(spriteBatch, new Vector2(x, y + yStep * 2), "AddRemoveTime: " + GameMain.World.AddRemoveTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.AddRemoveTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + DrawString(spriteBatch, new Vector2(x, y + yStep * 3), "NewContactsTime: " + GameMain.World.NewContactsTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.NewContactsTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + DrawString(spriteBatch, new Vector2(x, y + yStep * 4), "ContactsUpdateTime: " + GameMain.World.ContactsUpdateTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ContactsUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + DrawString(spriteBatch, new Vector2(x, y + yStep * 5), "SolveUpdateTime: " + GameMain.World.SolveUpdateTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.SolveUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); } } if (GameMain.DebugDraw && !Submarine.Unloading && !(Screen.Selected is RoundSummaryScreen)) { - float y = startY + 15 * yScale; - DrawString(spriteBatch, new Vector2(10, y), - "Physics: " + GameMain.World.UpdateTime, - Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); - - y += 15 * yScale; - DrawString(spriteBatch, new Vector2(10, y), - $"Bodies: {GameMain.World.BodyList.Count} ({GameMain.World.BodyList.Count(b => b != null && b.Awake && b.Enabled)} awake, {GameMain.World.BodyList.Count(b => b != null && b.Awake && b.BodyType == BodyType.Dynamic && b.Enabled)} dynamic)", - Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); + float y = startY + yStep * 6; if (Screen.Selected.Cam != null) { - y += 15 * yScale; + y += yStep; DrawString(spriteBatch, new Vector2(10, y), "Camera pos: " + Screen.Selected.Cam.Position.ToPoint() + ", zoom: " + Screen.Selected.Cam.Zoom, Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); @@ -392,23 +412,18 @@ namespace Barotrauma if (Submarine.MainSub != null) { - y += 15 * yScale; + y += yStep; DrawString(spriteBatch, new Vector2(10, y), "Sub pos: " + Submarine.MainSub.Position.ToPoint(), Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); } - y += 20 * yScale; - DrawString(spriteBatch, new Vector2(10, y), - "Particle count: " + GameMain.ParticleManager.ParticleCount + "/" + GameMain.ParticleManager.MaxParticles, - Color.Lerp(GUIStyle.Green, GUIStyle.Red, (GameMain.ParticleManager.ParticleCount / (float)GameMain.ParticleManager.MaxParticles)), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - if (loadedSpritesText == null || DateTime.Now > loadedSpritesUpdateTime) { loadedSpritesText = "Loaded sprites: " + Sprite.LoadedSprites.Count() + "\n(" + Sprite.LoadedSprites.Select(s => s.FilePath).Distinct().Count() + " unique textures)"; loadedSpritesUpdateTime = DateTime.Now + new TimeSpan(0, 0, seconds: 5); } - y += 25 * yScale; + y += yStep * 2; DrawString(spriteBatch, new Vector2(10, y), loadedSpritesText, Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); if (debugDrawSounds) @@ -416,21 +431,21 @@ namespace Barotrauma float soundTextY = 0; DrawString(spriteBatch, new Vector2(500, soundTextY), "Sounds (Ctrl+S to hide): ", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); - soundTextY += 15 * yScale; + soundTextY += yStep; DrawString(spriteBatch, new Vector2(500, soundTextY), "Current playback amplitude: " + GameMain.SoundManager.PlaybackAmplitude.ToString(), Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); - soundTextY += 15 * yScale; + soundTextY += yStep; DrawString(spriteBatch, new Vector2(500, soundTextY), "Compressed dynamic range gain: " + GameMain.SoundManager.CompressionDynamicRangeGain.ToString(), Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); - soundTextY += 15 * yScale; + soundTextY += yStep; DrawString(spriteBatch, new Vector2(500, soundTextY), "Loaded sounds: " + GameMain.SoundManager.LoadedSoundCount + " (" + GameMain.SoundManager.UniqueLoadedSoundCount + " unique)", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); - soundTextY += 15 * yScale; + soundTextY += yStep; for (int i = 0; i < SoundManager.SOURCE_COUNT; i++) { @@ -479,7 +494,7 @@ namespace Barotrauma } DrawString(spriteBatch, new Vector2(500, soundTextY), soundStr, clr, Color.Black * 0.5f, 0, GUIStyle.SmallFont); - soundTextY += 15 * yScale; + soundTextY += yStep; } } else @@ -1981,7 +1996,7 @@ namespace Barotrauma var element = new GUIFrame(new RectTransform(new Vector2(0.22f, 1), inputArea.RectTransform) { MinSize = new Point(50, 0), MaxSize = new Point(150, 50) }, style: null); new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), RectComponentLabels[i], font: font, textAlignment: Alignment.CenterLeft); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), - GUINumberInput.NumberType.Int) + NumberType.Int) { Font = font }; @@ -2025,7 +2040,7 @@ namespace Barotrauma var element = new GUIFrame(new RectTransform(new Vector2(0.45f, 1), inputArea.RectTransform), style: null); new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), VectorComponentLabels[i], font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), - GUINumberInput.NumberType.Int) + NumberType.Int) { Font = GUIStyle.SmallFont }; @@ -2055,7 +2070,7 @@ namespace Barotrauma { var element = new GUIFrame(new RectTransform(new Vector2(0.45f, 1), inputArea.RectTransform), style: null); new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), VectorComponentLabels[i], font: font, textAlignment: Alignment.CenterLeft); - GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Float) { Font = font }; + GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), NumberType.Float) { Font = font }; switch (i) { case 0: @@ -2425,8 +2440,7 @@ namespace Barotrauma verificationTextTag: GameMain.GameSession == null ? "PauseMenuQuitVerificationEditor" : "PauseMenuQuitVerification", action: () => { - // In the first campaign round we need to save the start items. - GameMain.QuitToMainMenu(save: GameMain.GameSession.GameMode is SinglePlayerCampaign campaign && campaign.IsFirstRound); + GameMain.QuitToMainMenu(save: false); }); } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs index b7265c76f..7e049e601 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs @@ -7,11 +7,6 @@ namespace Barotrauma { class GUINumberInput : GUIComponent { - public enum NumberType - { - Int, Float - } - public delegate void OnValueEnteredHandler(GUINumberInput numberInput); public OnValueEnteredHandler OnValueEntered; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 303a207fd..d54f1a9f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -1546,7 +1546,7 @@ namespace Barotrauma { RelativeSpacing = 0.02f }; - amountInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), shoppingCrateAmountGroup.RectTransform), GUINumberInput.NumberType.Int) + amountInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), shoppingCrateAmountGroup.RectTransform), NumberType.Int) { MinValueInt = 0, MaxValueInt = GetMaxAvailable(pi.ItemPrefab, containingTab), diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index d40a43dc0..8a71ba121 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -1064,7 +1064,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), GUINumberInput.NumberType.Int, hidePlusMinusButtons: true) + GUINumberInput transferAmountInput = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), inputLayout.RectTransform), NumberType.Int, hidePlusMinusButtons: true) { MinValueInt = 0 }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index db6a05ff0..188c91bed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -1037,6 +1037,11 @@ namespace Barotrauma { GUI.SetSavingIndicatorState(true); + if (GameSession.Submarine != null && !GameSession.Submarine.Removed) + { + GameSession.SubmarineInfo = new SubmarineInfo(GameSession.Submarine); + } + // Update store stock when saving and quitting in an outpost (normally updated when CampaignMode.End() is called) if (GameSession?.Campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedOutpost && spCampaign.Map?.CurrentLocation != null && spCampaign.CargoManager != null) { @@ -1163,7 +1168,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?template=bug_report.md", + UserData = "https://github.com/Regalis11/Barotrauma/issues/new/choose", OnClicked = (btn, userdata) => { ShowOpenUrlInWebBrowserPrompt(userdata as string); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 7e7506931..382c43a58 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -92,7 +92,7 @@ namespace Barotrauma break; case "crew": GameMain.GameSession.CrewManager = new CrewManager(subElement, true); - ActiveOrdersElement = element.GetChildElement("activeorders"); + ActiveOrdersElement = subElement.GetChildElement("activeorders"); break; case "map": map = Map.Load(this, subElement, Settings); @@ -461,6 +461,7 @@ namespace Barotrauma if (success) { + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); SaveUtil.SaveGame(GameMain.GameSession.SavePath); } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs index d903016ad..8428d53ff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs @@ -321,7 +321,7 @@ namespace Barotrauma.Items.Components { GUILayoutGroup layout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.08f), parent), isHorizontal: true); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), layout.RectTransform), label); - GUINumberInput input = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), layout.RectTransform), GUINumberInput.NumberType.Int) { IntValue = defaultValue }; + GUINumberInput input = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), layout.RectTransform), NumberType.Int) { IntValue = defaultValue }; return input; } @@ -329,7 +329,7 @@ namespace Barotrauma.Items.Components { GUILayoutGroup layout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.08f), parent), isHorizontal: true); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), layout.RectTransform), label); - GUINumberInput input = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), layout.RectTransform), GUINumberInput.NumberType.Float) { FloatValue = defaultValue, DecimalsToDisplay = 2 }; + GUINumberInput input = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), layout.RectTransform), NumberType.Float) { FloatValue = defaultValue, DecimalsToDisplay = 2 }; return input; } @@ -341,7 +341,7 @@ namespace Barotrauma.Items.Components for (var i = 0; i < values.Length; i++) { float value = values[i]; - GUINumberInput input = new GUINumberInput(new RectTransform(new Vector2(0.5f / values.Length, 1f), layout.RectTransform), GUINumberInput.NumberType.Float) + GUINumberInput input = new GUINumberInput(new RectTransform(new Vector2(0.5f / values.Length, 1f), layout.RectTransform), NumberType.Float) { FloatValue = value, DecimalsToDisplay = 2, MinValueFloat = min, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 5fe3adde1..b1d894203 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -280,9 +280,9 @@ namespace Barotrauma.Items.Components transformedItemPos += new Vector2(item.Rect.X, item.Rect.Y); if (item.Submarine != null) { transformedItemPos += item.Submarine.DrawPosition; } - if (Math.Abs(item.Rotation) > 0.01f) + if (Math.Abs(item.RotationRad) > 0.01f) { - Matrix transform = Matrix.CreateRotationZ(MathHelper.ToRadians(-item.Rotation)); + Matrix transform = Matrix.CreateRotationZ(-item.RotationRad); transformedItemPos = Vector2.Transform(transformedItemPos - item.DrawPosition, transform) + item.DrawPosition; transformedItemInterval = Vector2.Transform(transformedItemInterval, transform); transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 52cf7fef1..337a21274 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -56,9 +56,7 @@ namespace Barotrauma.Items.Components } else { - Vector2 pos = item.DrawPosition; - if (item.Submarine != null) { pos -= item.Submarine.DrawPosition; } - Light.Position = pos; + Light.Position = item.Position; } PhysicsBody body = Light.ParentBody; if (body != null) @@ -68,7 +66,7 @@ namespace Barotrauma.Items.Components } else { - Light.Rotation = -Rotation - MathHelper.ToRadians(item.Rotation); + Light.Rotation = -Rotation - item.RotationRad; Light.LightSpriteEffect = item.SpriteEffects; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index 2a6d4328e..52c211c18 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -240,7 +240,7 @@ namespace Barotrauma.Items.Components private bool OnActivateButtonClicked(GUIButton button, object obj) { var disallowedItem = inputContainer.Inventory.FindItem(i => !i.AllowDeconstruct, recursive: false); - if (disallowedItem != null) + if (disallowedItem != null && !DeconstructItemsSimultaneously) { int index = inputContainer.Inventory.FindIndex(disallowedItem); if (index >= 0 && index < inputContainer.Inventory.visualSlots.Length) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 87289c772..d14a9b237 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -613,7 +613,7 @@ namespace Barotrauma.Items.Components if (hullData.Distort) { hullData.ReceivedOxygenAmount = Rand.Range(0.0f, 100.0f); - hullData.ReceivedWaterAmount = Rand.Range(0.0f, 1.0f); + hullData.ReceivedWaterAmount = Rand.Range(0.0f, 100.0f); } hullData.DistortionTimer = Rand.Range(1.0f, 10.0f); } @@ -681,7 +681,7 @@ namespace Barotrauma.Items.Components var sprite = GUIStyle.UIGlowSolidCircular.Value?.Sprite; float alpha = (MathF.Sin(blipState / maxBlipState * MathHelper.TwoPi) + 1.5f) * 0.5f; - if (sprite != null) + if (sprite != null && ShowHullIntegrity) { Vector2 spriteSize = sprite.size; Rectangle worldBorders = item.Submarine.GetDockedBorders(); @@ -1014,13 +1014,13 @@ namespace Barotrauma.Items.Components hullData.HullWaterAmount = 0.0f; foreach (Hull linkedHull in hullData.LinkedHulls) { - hullData.HullWaterAmount += Math.Min(linkedHull.WaterVolume / linkedHull.Volume, 1.0f); + hullData.HullWaterAmount += WaterDetector.GetWaterPercentage(linkedHull); } hullData.HullWaterAmount /= hullData.LinkedHulls.Count; } else { - hullData.HullWaterAmount = Math.Min(hull.WaterVolume / hull.Volume, 1.0f); + hullData.HullWaterAmount = WaterDetector.GetWaterPercentage(hull); } float gapOpenSum = 0.0f; @@ -1052,8 +1052,8 @@ namespace Barotrauma.Items.Components LocalizedString line3 = waterAmount == null ? TextManager.Get("MiniMapWaterLevelUnavailable") : - TextManager.AddPunctuation(':', TextManager.Get("MiniMapWaterLevel"), (int)Math.Round(waterAmount.Value * 100.0f) + "%"); - Color line3Color = waterAmount == null ? GUIStyle.Red : Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)waterAmount); + TextManager.AddPunctuation(':', TextManager.Get("MiniMapWaterLevel"), (int)Math.Round(waterAmount.Value) + "%"); + Color line3Color = waterAmount == null ? GUIStyle.Red : Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)waterAmount / 100.0f); SetTooltip(borderComponent.Rect.Center, header, line1, line2, line3, line1Color, line2Color, line3Color); } @@ -1188,7 +1188,8 @@ namespace Barotrauma.Items.Components if (hullsVisible && hullData.HullWaterAmount is { } waterAmount) { - if (!RequireWaterDetectors) { waterAmount = hull.WaterPercentage / 100.0f; } + if (!RequireWaterDetectors) { waterAmount = WaterDetector.GetWaterPercentage(hull); } + waterAmount /= 100.0f; if (hullFrame.Rect.Height * waterAmount > 1.0f) { RectangleF waterRect = new RectangleF(hullFrame.Rect.X, hullFrame.Rect.Y + hullFrame.Rect.Height * (1.0f - waterAmount), hullFrame.Rect.Width, hullFrame.Rect.Height * waterAmount); @@ -1327,7 +1328,7 @@ namespace Barotrauma.Items.Components pos.X += inflate; pos.Y += inflate; - sprite.Draw(spriteBatch, pos, item.SpriteColor, sprite.Origin, MathHelper.ToRadians(item.Rotation), spriteScale, item.SpriteEffects); + sprite.Draw(spriteBatch, pos, item.SpriteColor, sprite.Origin, item.RotationRad, spriteScale, item.SpriteEffects); void DrawAdditionalSprite(Vector2 basePos, Sprite addSprite, float rotation) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs index e50bbcc8d..f3bfe988b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs @@ -133,7 +133,6 @@ namespace Barotrauma.Items.Components partial void UpdateProjSpecific(float deltaTime) { - float rotationRad = MathHelper.ToRadians(item.Rotation); if (FlowPercentage < 0.0f) { foreach (var (position, emitter) in pumpOutEmitters) @@ -142,8 +141,8 @@ namespace Barotrauma.Items.Components //only emit "pump out" particles when underwater Vector2 relativeParticlePos = (item.WorldRect.Location.ToVector2() + position * item.Scale) - item.WorldPosition; - relativeParticlePos = MathUtils.RotatePoint(relativeParticlePos, item.FlippedX ? rotationRad : -rotationRad); - float angle = -rotationRad; + relativeParticlePos = MathUtils.RotatePoint(relativeParticlePos, item.FlippedX ? item.RotationRad : -item.RotationRad); + float angle = -item.RotationRad; if (item.FlippedX) { relativeParticlePos.X = -relativeParticlePos.X; @@ -163,8 +162,8 @@ namespace Barotrauma.Items.Components foreach (var (position, emitter) in pumpInEmitters) { Vector2 relativeParticlePos = (item.WorldRect.Location.ToVector2() + position * item.Scale) - item.WorldPosition; - relativeParticlePos = MathUtils.RotatePoint(relativeParticlePos, item.FlippedX ? rotationRad : -rotationRad); - float angle = -rotationRad; + relativeParticlePos = MathUtils.RotatePoint(relativeParticlePos, item.FlippedX ? item.RotationRad : -item.RotationRad); + float angle = -item.RotationRad; if (item.FlippedX) { relativeParticlePos.X = -relativeParticlePos.X; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index 5d3e959c3..d1a295c91 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -1,11 +1,10 @@ -using System; -using Barotrauma.Networking; +using Barotrauma.Networking; using Barotrauma.Particles; using Barotrauma.Sounds; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using System; using System.Collections.Generic; -using System.Xml.Linq; namespace Barotrauma.Items.Components { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index 36713eab5..9526f7f63 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -92,6 +92,8 @@ namespace Barotrauma.Items.Components { if (target == null || target.Removed) { return; } if (target.ParentInventory != null) { return; } + if (source is Limb limb && limb.Removed) { return; } + if (source is Entity e && e.Removed) { return; } Vector2 startPos = GetSourcePos(); startPos.Y = -startPos.Y; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index 1853d569c..706105771 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -45,7 +45,7 @@ namespace Barotrauma.Items.Components }; new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), TextManager.Get(ciElement.Label).Fallback(ciElement.Label)); - if (!ciElement.IsIntegerInput) + if (!ciElement.IsNumberInput) { var textBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), ciElement.Signal, style: "GUITextBoxNoIcon") { @@ -77,29 +77,71 @@ namespace Barotrauma.Items.Components } else { - int.TryParse(ciElement.Signal, out int signal); - var numberInput = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), GUINumberInput.NumberType.Int) + GUINumberInput numberInput = null; + if (ciElement.NumberType == NumberType.Float) { - UserData = ciElement, - MinValueInt = ciElement.NumberInputMin, - MaxValueInt = ciElement.NumberInputMax, - IntValue = Math.Clamp(signal, ciElement.NumberInputMin, ciElement.NumberInputMax) - }; - //reset size restrictions set by the Style to make sure the elements can fit the interface - numberInput.RectTransform.MinSize = numberInput.LayoutGroup.RectTransform.MinSize = new Point(0, 0); - numberInput.RectTransform.MaxSize = numberInput.LayoutGroup.RectTransform.MaxSize = new Point(int.MaxValue, int.MaxValue); - numberInput.OnValueChanged += (ni) => + TryParseFloatInvariantCulture(ciElement.Signal, out float floatSignal); + TryParseFloatInvariantCulture(ciElement.NumberInputMin, out float numberInputMin); + TryParseFloatInvariantCulture(ciElement.NumberInputMax, out float numberInputMax); + TryParseFloatInvariantCulture(ciElement.NumberInputStep, out float numberInputStep); + numberInput = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), NumberType.Float) + { + UserData = ciElement, + MinValueFloat = numberInputMin, + MaxValueFloat = numberInputMax, + FloatValue = Math.Clamp(floatSignal, numberInputMin, numberInputMax), + DecimalsToDisplay = ciElement.NumberInputDecimalPlaces, + valueStep = numberInputStep, + OnValueChanged = (ni) => + { + if (GameMain.Client == null) + { + ValueChanged(ni.UserData as CustomInterfaceElement, ni.FloatValue); + } + else + { + item.CreateClientEvent(this); + } + } + }; + } + else if (ciElement.NumberType == NumberType.Int) { - if (GameMain.Client == null) + int.TryParse(ciElement.Signal, out int intSignal); + int.TryParse(ciElement.NumberInputMin, out int numberInputMin); + int.TryParse(ciElement.NumberInputMax, out int numberInputMax); + TryParseFloatInvariantCulture(ciElement.NumberInputStep, out float numberInputStep); + numberInput = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), NumberType.Int) { - ValueChanged(ni.UserData as CustomInterfaceElement, ni.IntValue); - } - else - { - item.CreateClientEvent(this); - } - }; - uiElements.Add(numberInput); + UserData = ciElement, + MinValueInt = numberInputMin, + MaxValueInt = numberInputMax, + IntValue = Math.Clamp(intSignal, numberInputMin, numberInputMax), + valueStep = numberInputStep, + OnValueChanged = (ni) => + { + if (GameMain.Client == null) + { + ValueChanged(ni.UserData as CustomInterfaceElement, ni.IntValue); + } + else + { + item.CreateClientEvent(this); + } + } + }; + } + else + { + DebugConsole.ShowError($"Error creating a CustomInterface component: unexpected NumberType \"{(ciElement.NumberType.HasValue ? ciElement.NumberType.Value.ToString() : "none")}\""); + } + if (numberInput != null) + { + //reset size restrictions set by the Style to make sure the elements can fit the interface + numberInput.RectTransform.MinSize = numberInput.LayoutGroup.RectTransform.MinSize = new Point(0, 0); + numberInput.RectTransform.MaxSize = numberInput.LayoutGroup.RectTransform.MaxSize = new Point(int.MaxValue, int.MaxValue); + uiElements.Add(numberInput); + } } } else if (ciElement.ContinuousSignal) @@ -293,7 +335,7 @@ namespace Barotrauma.Items.Components } else if (uiElements[i] is GUINumberInput ni) { - if (ni.InputType == GUINumberInput.NumberType.Int) + if (ni.InputType == NumberType.Int) { int.TryParse(customInterfaceElementList[i].Signal, out int value); ni.IntValue = value; @@ -307,18 +349,28 @@ namespace Barotrauma.Items.Components //extradata contains an array of buttons clicked by the player (or nothing if the player didn't click anything) for (int i = 0; i < customInterfaceElementList.Count; i++) { - if (customInterfaceElementList[i].HasPropertyName) + var element = customInterfaceElementList[i]; + if (element.HasPropertyName) { - if (!customInterfaceElementList[i].IsIntegerInput) + if (!element.IsNumberInput) { msg.Write(((GUITextBox)uiElements[i]).Text); } else { - msg.Write(((GUINumberInput)uiElements[i]).IntValue.ToString()); + switch (element.NumberType) + { + case NumberType.Float: + msg.Write(((GUINumberInput)uiElements[i]).FloatValue.ToString()); + break; + case NumberType.Int: + default: + msg.Write(((GUINumberInput)uiElements[i]).IntValue.ToString()); + break; + } } } - else if (customInterfaceElementList[i].ContinuousSignal) + else if (element.ContinuousSignal) { msg.Write(((GUITickBox)uiElements[i]).Selected); } @@ -333,29 +385,38 @@ namespace Barotrauma.Items.Components { for (int i = 0; i < customInterfaceElementList.Count; i++) { - if (customInterfaceElementList[i].HasPropertyName) + var element = customInterfaceElementList[i]; + if (element.HasPropertyName) { - if (!customInterfaceElementList[i].IsIntegerInput) + string newValue = msg.ReadString(); + if (!element.IsNumberInput) { - TextChanged(customInterfaceElementList[i], msg.ReadString()); + TextChanged(element, newValue); } else { - int.TryParse(msg.ReadString(), out int value); - ValueChanged(customInterfaceElementList[i], value); + switch (element.NumberType) + { + case NumberType.Int when int.TryParse(newValue, out int value): + ValueChanged(element, value); + break; + case NumberType.Float when TryParseFloatInvariantCulture(newValue, out float value): + ValueChanged(element, value); + break; + } } } else { bool elementState = msg.ReadBoolean(); - if (customInterfaceElementList[i].ContinuousSignal) + if (element.ContinuousSignal) { ((GUITickBox)uiElements[i]).Selected = elementState; - TickBoxToggled(customInterfaceElementList[i], elementState); + TickBoxToggled(element, elementState); } else if (elementState) { - ButtonClicked(customInterfaceElementList[i]); + ButtonClicked(element); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index e7cc950ab..bda201aa8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -352,7 +352,7 @@ namespace Barotrauma foreach (var decorativeSprite in Prefab.DecorativeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flippedX && Prefab.CanSpriteFlipX ? rotationRad : -rotationRad) * Scale; + 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, @@ -376,17 +376,17 @@ namespace Barotrauma } if (color.A > 0) { - activeSprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, origin, rotationRad, Scale, activeSprite.effects, depth); + activeSprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, origin, RotationRad, Scale, activeSprite.effects, depth); if (fadeInBrokenSprite != null) { float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f); - fadeInBrokenSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + fadeInBrokenSprite.Offset.ToVector2() * Scale, color * fadeInBrokenSpriteAlpha, origin, rotationRad, Scale, activeSprite.effects, d); + fadeInBrokenSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + fadeInBrokenSprite.Offset.ToVector2() * Scale, color * fadeInBrokenSpriteAlpha, origin, RotationRad, Scale, activeSprite.effects, d); } } if (Infector != null && (Infector.ParentBallastFlora.HasBrokenThrough || BallastFloraBehavior.AlwaysShowBallastFloraSprite)) { - 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); + 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) { @@ -394,11 +394,11 @@ namespace Barotrauma 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; + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flipX ^ flipY ? RotationRad : -RotationRad) * Scale; if (flipX) { offset.X = -offset.X; } if (flipY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, - rotationRad + rot, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, + RotationRad + rot, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); } } @@ -445,7 +445,7 @@ namespace Barotrauma { 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; + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -RotationRad) * Scale; if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } var ca = (float)Math.Cos(-body.Rotation); @@ -466,7 +466,7 @@ namespace Barotrauma { 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; + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -RotationRad) * Scale; if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs index 475553ddf..f92c7a30f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs @@ -60,12 +60,6 @@ namespace Barotrauma } public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) { IsDisposed = true; WallEdgeBuffer?.Dispose(); @@ -482,12 +476,6 @@ namespace Barotrauma } public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) { foreach (var vertexBuffer in vertexBuffers) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs index ea9eebd9c..a053bcc73 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs @@ -230,14 +230,6 @@ namespace Barotrauma public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (!disposing) return; - if (WaterEffect != null) { WaterEffect.Dispose(); @@ -250,6 +242,5 @@ namespace Barotrauma basicEffect = null; } } - } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index d1764fd10..e49b2cf55 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -11,6 +11,18 @@ namespace Barotrauma.Lights { class LightManager { + /// + /// How many light sources are allowed to recalculate their light volumes per frame. + /// Pending calculations will be done on subsequent frames, starting from the light sources that have been waiting for a recalculation the longest. + /// + const int MaxLightVolumeRecalculationsPerFrame = 5; + + /// + /// If zoomed further out than this, characters no longer obstruct lights behind them. + /// Improves performance, and isn't very noticeable if we do it after zoomed far out enough. + /// + const float ObstructLightsBehindCharactersZoomThreshold = 0.5f; + public static Entity ViewTarget { get; set; } private float currLightMapScale; @@ -59,6 +71,8 @@ namespace Barotrauma.Lights private Vector2 losOffset; + private int recalculationCount; + public IEnumerable Lights { get { return lights; } @@ -151,6 +165,9 @@ namespace Barotrauma.Lights } private readonly List activeLights = new List(capacity: 100); + private readonly List activeLightsWithLightVolume = new List(capacity: 100); + + public static int ActiveLightCount { get; private set; } public void Update(float deltaTime) { @@ -180,11 +197,13 @@ namespace Barotrauma.Lights Rectangle viewRect = cam.WorldView; viewRect.Y -= cam.WorldView.Height; //check which lights need to be drawn + recalculationCount = 0; activeLights.Clear(); foreach (LightSource light in lights) { if (!light.Enabled) { continue; } if ((light.Color.A < 1 || light.Range < 1.0f) && !light.LightSourceParams.OverrideLightSpriteAlpha.HasValue) { continue; } + if (light.ParentBody != null) { light.ParentBody.UpdateDrawPosition(); @@ -205,8 +224,44 @@ namespace Barotrauma.Lights range = Math.Max(Math.Max(spriteRange, targetSize), range); } if (!MathUtils.CircleIntersectsRectangle(light.WorldPosition, range, viewRect)) { continue; } - activeLights.Add(light); + + light.Priority = lightPriority(range, light); + + int i = 0; + while (i < activeLights.Count && light.Priority < activeLights[i].Priority) + { + i++; + } + activeLights.Insert(i, light); } + ActiveLightCount = activeLights.Count; + + float lightPriority(float range, LightSource light) + { + return + range * + ((Character.Controlled?.Submarine != null && light.ParentSub == Character.Controlled?.Submarine) ? 2.0f : 1.0f) * + (light.CastShadows ? 10.0f : 1.0f) * + (light.LightSourceParams.OverrideLightSpriteAlpha ?? (light.Color.A / 255.0f)); + } + + //find the lights with an active light volume + activeLightsWithLightVolume.Clear(); + foreach (var activeLight in activeLights) + { + if (activeLight.Range < 1.0f || activeLight.Color.A < 1 || activeLight.CurrentBrightness <= 0.0f) { continue; } + activeLightsWithLightVolume.Add(activeLight); + } + + //remove some lights with a light volume if there's too many of them + if (activeLightsWithLightVolume.Count > GameSettings.CurrentConfig.Graphics.VisibleLightLimit) + { + for (int i = GameSettings.CurrentConfig.Graphics.VisibleLightLimit; i < activeLightsWithLightVolume.Count; i++) + { + activeLights.Remove(activeLightsWithLightVolume[i]); + } + } + activeLights.Sort((l1, l2) => l1.LastRecalculationTime.CompareTo(l2.LastRecalculationTime)); //draw light sprites attached to characters //render into a separate rendertarget using alpha blending (instead of on top of everything else with alpha blending) @@ -235,7 +290,7 @@ namespace Barotrauma.Lights { if (!light.IsBackground || light.CurrentBrightness <= 0.0f) { continue; } light.DrawSprite(spriteBatch, cam); - light.DrawLightVolume(spriteBatch, lightEffect, transform); + light.DrawLightVolume(spriteBatch, lightEffect, transform, recalculationCount < MaxLightVolumeRecalculationsPerFrame, ref recalculationCount); } GameMain.ParticleManager.Draw(spriteBatch, true, null, Particles.ParticleBlendState.Additive); spriteBatch.End(); @@ -243,14 +298,6 @@ namespace Barotrauma.Lights //draw a black rectangle on hulls to hide background lights behind subs //--------------------------------------------------------------------------------------------------- - /*if (backgroundObstructor != null) - { - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied); - spriteBatch.Draw(backgroundObstructor, new Rectangle(0, 0, - (int)(GameMain.GraphicsWidth * currLightMapScale), (int)(GameMain.GraphicsHeight * currLightMapScale)), Color.Black); - spriteBatch.End(); - }*/ - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, transformMatrix: spriteBatchTransform); Dictionary visibleHulls = GetVisibleHulls(cam); foreach (KeyValuePair hull in visibleHulls) @@ -292,41 +339,44 @@ namespace Barotrauma.Lights //draw characters to obstruct the highlighted items/characters and light sprites //--------------------------------------------------------------------------------------------------- - - SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidVertexColor"]; - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, effect: SolidColorEffect, transformMatrix: spriteBatchTransform); - foreach (Character character in Character.CharacterList) + if (cam.Zoom > ObstructLightsBehindCharactersZoomThreshold) { - if (character.CurrentHull == null || !character.Enabled || !character.IsVisible) { continue; } - if (Character.Controlled?.FocusedCharacter == character) { continue; } - Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ? - Color.Black : - character.CurrentHull.AmbientLight.Multiply(character.CurrentHull.AmbientLight.A / 255.0f).Opaque(); - foreach (Limb limb in character.AnimController.Limbs) + SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidVertexColor"]; + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, effect: SolidColorEffect, transformMatrix: spriteBatchTransform); + foreach (Character character in Character.CharacterList) { - if (limb.DeformSprite != null) { continue; } - limb.Draw(spriteBatch, cam, lightColor); + if (character.CurrentHull == null || !character.Enabled || !character.IsVisible) { continue; } + if (Character.Controlled?.FocusedCharacter == character) { continue; } + Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ? + Color.Black : + character.CurrentHull.AmbientLight.Multiply(character.CurrentHull.AmbientLight.A / 255.0f).Opaque(); + foreach (Limb limb in character.AnimController.Limbs) + { + if (limb.DeformSprite != null) { continue; } + limb.Draw(spriteBatch, cam, lightColor); + } } - } - spriteBatch.End(); + spriteBatch.End(); - DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShaderSolidVertexColor"]; - DeformableSprite.Effect.CurrentTechnique.Passes[0].Apply(); - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, transformMatrix: spriteBatchTransform); - foreach (Character character in Character.CharacterList) - { - if (character.CurrentHull == null || !character.Enabled || !character.IsVisible) { continue; } - if (Character.Controlled?.FocusedCharacter == character) { continue; } - Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ? - Color.Black : - character.CurrentHull.AmbientLight.Multiply(character.CurrentHull.AmbientLight.A / 255.0f).Opaque(); - foreach (Limb limb in character.AnimController.Limbs) + DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShaderSolidVertexColor"]; + DeformableSprite.Effect.CurrentTechnique.Passes[0].Apply(); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, transformMatrix: spriteBatchTransform); + foreach (Character character in Character.CharacterList) { - if (limb.DeformSprite == null) { continue; } - limb.Draw(spriteBatch, cam, lightColor); + if (character.CurrentHull == null || !character.Enabled || !character.IsVisible) { continue; } + if (Character.Controlled?.FocusedCharacter == character) { continue; } + Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ? + Color.Black : + character.CurrentHull.AmbientLight.Multiply(character.CurrentHull.AmbientLight.A / 255.0f).Opaque(); + foreach (Limb limb in character.AnimController.Limbs) + { + if (limb.DeformSprite == null) { continue; } + limb.Draw(spriteBatch, cam, lightColor); + } } + spriteBatch.End(); } - spriteBatch.End(); + DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShader"]; graphics.BlendState = BlendState.Additive; @@ -344,7 +394,7 @@ namespace Barotrauma.Lights foreach (LightSource light in activeLights) { if (light.IsBackground || light.CurrentBrightness <= 0.0f) { continue; } - light.DrawLightVolume(spriteBatch, lightEffect, transform); + light.DrawLightVolume(spriteBatch, lightEffect, transform, recalculationCount < MaxLightVolumeRecalculationsPerFrame, ref recalculationCount); } lightEffect.World = transform; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 0eacc74f3..5a4454ccf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -205,7 +205,7 @@ namespace Barotrauma.Lights private VertexPositionColorTexture[] vertices; private short[] indices; - private List hullsInRange; + private readonly List hullsInRange; public Texture2D texture; @@ -246,9 +246,9 @@ namespace Barotrauma.Lights } //when were the vertices of the light volume last calculated - private float lastRecalculationTime; + public float LastRecalculationTime { get; private set; } - private Dictionary diffToSub; + private readonly Dictionary diffToSub; private DynamicVertexBuffer lightVolumeBuffer; private DynamicIndexBuffer lightVolumeIndexBuffer; @@ -376,6 +376,8 @@ namespace Barotrauma.Lights } } + public float Priority; + private Vector2 lightTextureTargetSize; public Vector2 LightTextureTargetSize @@ -423,7 +425,7 @@ namespace Barotrauma.Lights public bool Enabled = true; - private ISerializableEntity conditionalTarget; + private readonly ISerializableEntity conditionalTarget; private readonly PropertyConditional.Comparison comparison; private readonly List conditionals = new List(); @@ -561,7 +563,7 @@ namespace Barotrauma.Lights foreach (var ch in chList.List) { - if (ch.LastVertexChangeTime > lastRecalculationTime && !chList.IsHidden.Contains(ch)) + if (ch.LastVertexChangeTime > LastRecalculationTime && !chList.IsHidden.Contains(ch)) { NeedsRecalculation = true; break; @@ -1289,7 +1291,7 @@ namespace Barotrauma.Lights } //visualize light recalculations - float timeSinceRecalculation = (float)Timing.TotalTime - lastRecalculationTime; + float timeSinceRecalculation = (float)Timing.TotalTime - LastRecalculationTime; if (timeSinceRecalculation < 0.1f) { GUI.DrawRectangle(spriteBatch, drawPos - Vector2.One * 10, Vector2.One * 20, GUIStyle.Red * (1.0f - timeSinceRecalculation * 10.0f), isFilled: true); @@ -1313,7 +1315,7 @@ namespace Barotrauma.Lights } } - public void DrawLightVolume(SpriteBatch spriteBatch, BasicEffect lightEffect, Matrix transform) + 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; } @@ -1338,8 +1340,9 @@ namespace Barotrauma.Lights CheckHullsInRange(); - if (NeedsRecalculation) + if (NeedsRecalculation && allowRecalculation) { + recalculationCount++; var verts = FindRaycastHits(); if (verts == null) { @@ -1352,7 +1355,7 @@ namespace Barotrauma.Lights CalculateLightVertices(verts); - lastRecalculationTime = (float)Timing.TotalTime; + LastRecalculationTime = (float)Timing.TotalTime; NeedsRecalculation = false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 4f0dfbe2c..651f40201 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -977,7 +977,7 @@ namespace Barotrauma Vector2 center = rectCenter + (connection.CenterPos + viewOffset) * zoom; if (viewArea.Contains(center) && connection.Biome != null) { - GUI.DrawString(spriteBatch, center, connection.Biome.Identifier + " (" + connection.Difficulty + ")", Color.White); + GUI.DrawString(spriteBatch, center, (connection.LevelData?.GenerationParams?.Identifier ?? connection.Biome.Identifier) + " (" + (int)connection.Difficulty + ")", Color.White); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index 5aa7c9197..ad05d0b20 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -16,7 +16,7 @@ namespace Barotrauma class SubmarinePreview : IDisposable { private SpriteRecorder spriteRecorder; - private SubmarineInfo submarineInfo; + private readonly SubmarineInfo submarineInfo; private Camera camera; private Task loadTask; private volatile bool isDisposed; @@ -66,7 +66,7 @@ namespace Barotrauma public static void Close() { - instance?.Dispose(); + instance?.Dispose(); instance = null; } private SubmarinePreview(SubmarineInfo subInfo) @@ -655,7 +655,8 @@ namespace Barotrauma previewFrame.RectTransform.Parent = null; previewFrame = null; } - spriteRecorder?.Dispose(); + spriteRecorder?.Dispose(); spriteRecorder = null; + camera?.Dispose(); camera = null; isDisposed = true; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs index 75401668a..1a0efc16c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs @@ -146,26 +146,19 @@ namespace Barotrauma.Networking } private bool disposed = false; - protected virtual void Dispose(bool disposing) - { - if (disposed) return; - - if (disposing) - { - if (WriteStream != null) - { - WriteStream.Flush(); - WriteStream.Close(); - WriteStream.Dispose(); - WriteStream = null; - } - } - disposed = true; - } - + public void Dispose() { - Dispose(true); + if (disposed) { return; } + + if (WriteStream != null) + { + WriteStream.Flush(); + WriteStream.Close(); + WriteStream.Dispose(); + WriteStream = null; + } + disposed = true; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 32fae9689..2e36c70de 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -3571,13 +3571,13 @@ namespace Barotrauma.Networking return true; }; - durationInputDays = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1.0f), durationContainer.RectTransform), GUINumberInput.NumberType.Int) + durationInputDays = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1.0f), durationContainer.RectTransform), NumberType.Int) { MinValueInt = 0, MaxValueFloat = 1000 }; new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), durationContainer.RectTransform), TextManager.Get("Days")); - durationInputHours = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1.0f), durationContainer.RectTransform), GUINumberInput.NumberType.Int) + durationInputHours = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1.0f), durationContainer.RectTransform), NumberType.Int) { MinValueInt = 0, MaxValueFloat = 24 diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs index e26337d66..050355919 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs @@ -126,7 +126,7 @@ namespace Barotrauma ToolTip = TextManager.Get("Karma." + propertyName + "ToolTip") }; - var numInput = new GUINumberInput(new RectTransform(new Vector2(0.3f, 1.0f), container.RectTransform), GUINumberInput.NumberType.Int) + var numInput = new GUINumberInput(new RectTransform(new Vector2(0.3f, 1.0f), container.RectTransform), NumberType.Int) { MinValueInt = min, MaxValueInt = max diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs index 2b72f5dfa..09cb93edc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -373,12 +373,6 @@ namespace Barotrauma.Networking OnDisconnect?.Invoke(disableReconnect); } - ~SteamP2PClientPeer() - { - OnDisconnect = null; - Close(); - } - protected override void SendMsgInternal(DeliveryMethod deliveryMethod, IWriteMessage msg) { Steamworks.P2PSend sendType; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index 580f16b8a..04c501909 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -447,12 +447,6 @@ namespace Barotrauma.Networking ChildServerRelay.Write(bufToSend); } - ~SteamP2POwnerPeer() - { - OnDisconnect = null; - Close(); - } - protected override void SendMsgInternal(DeliveryMethod deliveryMethod, IWriteMessage msg) { //not currently used by SteamP2POwnerPeer diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index 326c6f9a0..f03d7b099 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -32,7 +32,7 @@ namespace Barotrauma.Networking else if (GUIComponent is GUIDropDown dropdown) return dropdown.SelectedData; else if (GUIComponent is GUINumberInput numInput) { - if (numInput.InputType == GUINumberInput.NumberType.Int) { return numInput.IntValue; } else { return numInput.FloatValue; } + if (numInput.InputType == NumberType.Int) { return numInput.IntValue; } else { return numInput.FloatValue; } } return null; } @@ -56,7 +56,7 @@ namespace Barotrauma.Networking else if (GUIComponent is GUIDropDown dropdown) dropdown.SelectItem(value); else if (GUIComponent is GUINumberInput numInput) { - if (numInput.InputType == GUINumberInput.NumberType.Int) + if (numInput.InputType == NumberType.Int) { numInput.IntValue = (int)value; } @@ -480,15 +480,12 @@ namespace Barotrauma.Networking // game settings //-------------------------------------------------------------------------------- - var roundsTab = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), settingsTabs[(int)SettingsTab.Rounds].RectTransform, Anchor.Center)) - { - Stretch = true, - RelativeSpacing = 0.02f - }; + var roundsTab = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), settingsTabs[(int)SettingsTab.Rounds].RectTransform, Anchor.Center)) { }; + GUILayoutGroup playStyleLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), roundsTab.RectTransform)); // Play Style Selection - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUIStyle.SubHeadingFont); - var playstyleList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.16f), roundsTab.RectTransform)) + 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 @@ -510,11 +507,16 @@ namespace Barotrauma.Networking 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)); - var endVoteBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), + GUILayoutGroup sliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.35f), roundsTab.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(roundsTab, "ServerSettingsEndRoundVotesRequired", out slider, out sliderLabel); + CreateLabeledSlider(sliderLayout, "ServerSettingsEndRoundVotesRequired", out slider, out sliderLabel); LocalizedString endRoundLabel = sliderLabel.Text; slider.Step = 0.2f; @@ -527,11 +529,11 @@ namespace Barotrauma.Networking }; slider.OnMoved(slider, slider.BarScroll); - var respawnBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), + var respawnBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), sliderLayout.RectTransform), TextManager.Get("ServerSettingsAllowRespawning")); GetPropertyData(nameof(AllowRespawn)).AssignGUIComponent(respawnBox); - CreateLabeledSlider(roundsTab, "ServerSettingsRespawnInterval", out slider, out sliderLabel); + CreateLabeledSlider(sliderLayout, "ServerSettingsRespawnInterval", out slider, out sliderLabel); LocalizedString intervalLabel = sliderLabel.Text; slider.Range = new Vector2(10.0f, 600.0f); slider.StepValue = 10.0f; @@ -544,7 +546,7 @@ namespace Barotrauma.Networking }; slider.OnMoved(slider, slider.BarScroll); - var respawnLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), roundsTab.RectTransform), + var respawnLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), sliderLayout.RectTransform), isHorizontal: true); var minRespawnLayout @@ -611,12 +613,13 @@ namespace Barotrauma.Networking }; slider.OnMoved(slider, slider.BarScroll); + GUILayoutGroup losModeLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.14f), roundsTab.RectTransform)); - var losModeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.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.1f), roundsTab.RectTransform), + = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), losModeLayout.RectTransform), isHorizontal: true) { Stretch = true @@ -631,24 +634,29 @@ namespace Barotrauma.Networking } GetPropertyData(nameof(LosMode)).AssignGUIComponent(losModeRadioButtonGroup); - var traitorsMinPlayerCount = CreateLabeledNumberInput(roundsTab, "ServerSettingsTraitorsMinPlayerCount", 1, 16, "ServerSettingsTraitorsMinPlayerCountToolTip"); + GUILayoutGroup numberLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), roundsTab.RectTransform)) + { + Stretch = true + }; + + var traitorsMinPlayerCount = CreateLabeledNumberInput(numberLayout, "ServerSettingsTraitorsMinPlayerCount", 1, 16, "ServerSettingsTraitorsMinPlayerCountToolTip"); GetPropertyData(nameof(TraitorsMinPlayerCount)).AssignGUIComponent(traitorsMinPlayerCount); - var maximumTransferAmount = CreateLabeledNumberInput(roundsTab, "serversettingsmaximumtransferrequest", 0, CampaignMode.MaxMoney, "serversettingsmaximumtransferrequesttooltip"); + var maximumTransferAmount = CreateLabeledNumberInput(numberLayout, "serversettingsmaximumtransferrequest", 0, CampaignMode.MaxMoney, "serversettingsmaximumtransferrequesttooltip"); GetPropertyData(nameof(MaximumMoneyTransferRequest)).AssignGUIComponent(maximumTransferAmount); - var lootedMoneyDestination = CreateLabeledDropdown(roundsTab, "serversettingslootedmoneydestination", numElements: 2, "serversettingslootedmoneydestinationtooltip"); + 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 ragdollButtonBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsAllowRagdollButton")); + var ragdollButtonBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), numberLayout.RectTransform), TextManager.Get("ServerSettingsAllowRagdollButton")); GetPropertyData(nameof(AllowRagdollButton)).AssignGUIComponent(ragdollButtonBox); - var disableBotConversationsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsDisableBotConversations")); + var disableBotConversationsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), numberLayout.RectTransform), TextManager.Get("ServerSettingsDisableBotConversations")); GetPropertyData(nameof(DisableBotConversations)).AssignGUIComponent(disableBotConversationsBox); - var buttonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), roundsTab.RectTransform), isHorizontal: true) + GUILayoutGroup buttonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), roundsTab.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f @@ -755,7 +763,7 @@ namespace Barotrauma.Networking ExtraCargo.TryGetValue(ip, out int cargoVal); var amountInput = new GUINumberInput(new RectTransform(new Vector2(0.35f, 1.0f), itemFrame.RectTransform), - GUINumberInput.NumberType.Int, textAlignment: Alignment.CenterLeft) + NumberType.Int, textAlignment: Alignment.CenterLeft) { MinValueInt = 0, MaxValueInt = MaxExtraCargoItemsOfType, @@ -987,7 +995,7 @@ namespace Barotrauma.Networking { label.ToolTip = TextManager.Get(toolTipTag); } - var input = new GUINumberInput(new RectTransform(new Vector2(0.3f, 1.0f), container.RectTransform), GUINumberInput.NumberType.Int) + var input = new GUINumberInput(new RectTransform(new Vector2(0.3f, 1.0f), container.RectTransform), NumberType.Int) { MinValueInt = min, MaxValueInt = max diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index e9de25b54..72363e2fe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -2280,7 +2280,7 @@ namespace Barotrauma.CharacterEditor var colorLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), colorComponentLabels[i], font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), - GUINumberInput.NumberType.Int, relativeButtonAreaWidth: 0.25f) + NumberType.Int, relativeButtonAreaWidth: 0.25f) { Font = GUIStyle.SmallFont }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index 6027aa90a..3f61ef66d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -523,7 +523,7 @@ namespace Barotrauma.CharacterEditor { var element = new GUIFrame(new RectTransform(new Vector2(0.22f, 1), inputArea.RectTransform) { MinSize = new Point(50, 0), MaxSize = new Point(150, 50) }, style: null); new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), GUI.RectComponentLabels[i], font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft); - GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Int) + GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), NumberType.Int) { Font = GUIStyle.SmallFont }; @@ -863,7 +863,7 @@ namespace Barotrauma.CharacterEditor var limbTypeField = GUI.CreateEnumField(limbType, elementSize, GetCharacterEditorTranslation("LimbType"), group.RectTransform, font: GUIStyle.Font); var sourceRectField = GUI.CreateRectangleField(sourceRect ?? new Rectangle(0, 100 * LimbGUIElements.Count, 100, 100), elementSize, GetCharacterEditorTranslation("SourceRectangle"), group.RectTransform, font: GUIStyle.Font); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1), idField.RectTransform, Anchor.TopLeft), GetCharacterEditorTranslation("ID")); - new GUINumberInput(new RectTransform(new Vector2(0.5f, 1), idField.RectTransform, Anchor.TopRight), GUINumberInput.NumberType.Int) + new GUINumberInput(new RectTransform(new Vector2(0.5f, 1), idField.RectTransform, Anchor.TopRight), NumberType.Int) { MinValueInt = 0, MaxValueInt = byte.MaxValue, @@ -912,7 +912,7 @@ namespace Barotrauma.CharacterEditor }; var limb1Field = new GUIFrame(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), style: null); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1), limb1Field.RectTransform, Anchor.TopLeft), GetCharacterEditorTranslation("LimbWithIndex").Replace("[index]", "1")); - var limb1InputField = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1), limb1Field.RectTransform, Anchor.TopRight), GUINumberInput.NumberType.Int) + var limb1InputField = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1), limb1Field.RectTransform, Anchor.TopRight), NumberType.Int) { MinValueInt = 0, MaxValueInt = byte.MaxValue, @@ -920,7 +920,7 @@ namespace Barotrauma.CharacterEditor }; var limb2Field = new GUIFrame(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), style: null); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1), limb2Field.RectTransform, Anchor.TopLeft), GetCharacterEditorTranslation("LimbWithIndex").Replace("[index]", "2")); - var limb2InputField = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1), limb2Field.RectTransform, Anchor.TopRight), GUINumberInput.NumberType.Int) + var limb2InputField = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1), limb2Field.RectTransform, Anchor.TopRight), NumberType.Int) { MinValueInt = 0, MaxValueInt = byte.MaxValue, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs index 9e690bb7b..38202f624 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs @@ -38,9 +38,9 @@ namespace Barotrauma } // attach number inputs to our generated parent elements - var rInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[0].RectTransform), GUINumberInput.NumberType.Int) { IntValue = BackgroundColor.R }; - var gInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[1].RectTransform), GUINumberInput.NumberType.Int) { IntValue = BackgroundColor.G }; - var bInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[2].RectTransform), GUINumberInput.NumberType.Int) { IntValue = BackgroundColor.B }; + var rInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[0].RectTransform), NumberType.Int) { IntValue = BackgroundColor.R }; + var gInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[1].RectTransform), NumberType.Int) { IntValue = BackgroundColor.G }; + var bInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[2].RectTransform), NumberType.Int) { IntValue = BackgroundColor.B }; rInput.MinValueInt = gInput.MinValueInt = bInput.MinValueInt = 0; rInput.MaxValueInt = gInput.MaxValueInt = bInput.MaxValueInt = 255; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index 2e450a813..46b2e753f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -818,7 +818,7 @@ namespace Barotrauma } else if (type == typeof(float) || type == typeof(int)) { - GUINumberInput valueInput = new GUINumberInput(new RectTransform(Vector2.One, layout.RectTransform), GUINumberInput.NumberType.Float) { FloatValue = (float) (newValue ?? 0.0f) }; + GUINumberInput valueInput = new GUINumberInput(new RectTransform(Vector2.One, layout.RectTransform), NumberType.Float) { FloatValue = (float) (newValue ?? 0.0f) }; valueInput.OnValueChanged += component => { newValue = component.FloatValue; }; } else if (type == typeof(bool)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 146f41f13..64a24a84d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -40,6 +40,8 @@ namespace Barotrauma private readonly Color[] tunnelDebugColors = new Color[] { Color.White, Color.Cyan, Color.LightGreen, Color.Red, Color.LightYellow, Color.LightSeaGreen }; + private LevelData currentLevelData; + public LevelEditorScreen() { Cam = new Camera() @@ -59,8 +61,9 @@ namespace Barotrauma paramsList.OnSelected += (GUIComponent component, object obj) => { selectedParams = obj as LevelGenerationParams; + currentLevelData = LevelData.CreateRandom(seedBox.Text, generationParams: selectedParams); editorContainer.ClearChildren(); - SortLevelObjectsList(selectedParams); + SortLevelObjectsList(currentLevelData); new SerializableEntityEditor(editorContainer.Content.RectTransform, selectedParams, false, true, elementHeight: 20); return true; }; @@ -149,7 +152,7 @@ namespace Barotrauma { OnClicked = (button, userData) => { - if(seedBox == null) { return false; } + if (seedBox == null) { return false; } seedBox.Text = GetLevelSeed(); return true; } @@ -184,10 +187,10 @@ namespace Barotrauma bool wasLevelLoaded = Level.Loaded != null; Submarine.Unload(); GameMain.LightManager.ClearLights(); - LevelData levelData = LevelData.CreateRandom(seedBox.Text, generationParams: selectedParams); - levelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; - levelData.AllowInvalidOutpost = allowInvalidOutpost.Selected; - Level.Generate(levelData, mirror: mirrorLevel.Selected); + currentLevelData = LevelData.CreateRandom(seedBox.Text, generationParams: selectedParams); + currentLevelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; + currentLevelData.AllowInvalidOutpost = allowInvalidOutpost.Selected; + Level.Generate(currentLevelData, mirror: mirrorLevel.Selected); 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) { @@ -417,11 +420,11 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), commonnessContainer.RectTransform), TextManager.GetWithVariable("leveleditor.levelobjcommonness", "[leveltype]", selectedParams.Identifier.Value), textAlignment: Alignment.Center); - new GUINumberInput(new RectTransform(new Vector2(0.5f, 0.4f), commonnessContainer.RectTransform), GUINumberInput.NumberType.Float) + new GUINumberInput(new RectTransform(new Vector2(0.5f, 0.4f), commonnessContainer.RectTransform), NumberType.Float) { MinValueFloat = 0, MaxValueFloat = 100, - FloatValue = caveGenerationParams.GetCommonness(selectedParams, abyss: false), + FloatValue = caveGenerationParams.GetCommonness(currentLevelData, abyss: false), OnValueChanged = (numberInput) => { caveGenerationParams.OverrideCommonness[selectedParams.Identifier] = numberInput.FloatValue; @@ -482,7 +485,7 @@ namespace Barotrauma { var moduleCountGroup = new GUILayoutGroup(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(25 * GUI.Scale))), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), TextManager.Capitalize(moduleCount.Key.Value), textAlignment: Alignment.CenterLeft); - new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), GUINumberInput.NumberType.Int) + new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), NumberType.Int) { MinValueInt = 0, MaxValueInt = 100, @@ -542,7 +545,7 @@ namespace Barotrauma if (selectedParams != null) { availableIdentifiers.Add(selectedParams.Identifier); } foreach (var caveParam in CaveGenerationParams.CaveParams) { - if (selectedParams != null && caveParam.GetCommonness(selectedParams, abyss: false) <= 0.0f) { continue; } + if (selectedParams != null && caveParam.GetCommonness(currentLevelData, abyss: false) <= 0.0f) { continue; } availableIdentifiers.Add(caveParam.Identifier); } availableIdentifiers.Reverse(); @@ -557,11 +560,11 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), commonnessContainer.RectTransform), TextManager.GetWithVariable("leveleditor.levelobjcommonness", "[leveltype]", paramsId.Value), textAlignment: Alignment.Center); - new GUINumberInput(new RectTransform(new Vector2(0.5f, 0.4f), commonnessContainer.RectTransform), GUINumberInput.NumberType.Float) + new GUINumberInput(new RectTransform(new Vector2(0.5f, 0.4f), commonnessContainer.RectTransform), NumberType.Float) { MinValueFloat = 0, MaxValueFloat = 100, - FloatValue = selectedParams.Identifier == paramsId ? levelObjectPrefab.GetCommonness(selectedParams) : levelObjectPrefab.GetCommonness(CaveGenerationParams.CaveParams.Find(p => p.Identifier == paramsId)), + FloatValue = selectedParams.Identifier == paramsId ? levelObjectPrefab.GetCommonness(currentLevelData) : levelObjectPrefab.GetCommonness(CaveGenerationParams.CaveParams.Find(p => p.Identifier == paramsId)), OnValueChanged = (numberInput) => { levelObjectPrefab.OverrideCommonness[paramsId] = numberInput.FloatValue; @@ -620,7 +623,7 @@ namespace Barotrauma childObj.AllowedNames = dropdown.SelectedDataMultiple.Select(d => ((LevelObjectPrefab)d).Name).ToList(); return true; }; - new GUINumberInput(new RectTransform(new Vector2(0.2f, 1.0f), paddedFrame.RectTransform), GUINumberInput.NumberType.Int) + new GUINumberInput(new RectTransform(new Vector2(0.2f, 1.0f), paddedFrame.RectTransform), NumberType.Int) { MinValueInt = 0, MaxValueInt = 10, @@ -630,7 +633,7 @@ namespace Barotrauma selectedChildObj.MaxCount = Math.Max(selectedChildObj.MaxCount, selectedChildObj.MinCount); } }.IntValue = childObj.MinCount; - new GUINumberInput(new RectTransform(new Vector2(0.2f, 1.0f), paddedFrame.RectTransform), GUINumberInput.NumberType.Int) + new GUINumberInput(new RectTransform(new Vector2(0.2f, 1.0f), paddedFrame.RectTransform), NumberType.Int) { MinValueInt = 0, MaxValueInt = 10, @@ -689,13 +692,13 @@ namespace Barotrauma buttonContainer.RectTransform.MinSize = buttonContainer.RectTransform.Children.First().MinSize; } - private void SortLevelObjectsList(LevelGenerationParams selectedParams) + private void SortLevelObjectsList(LevelData levelData) { //fade out levelobjects that don't spawn in this type of level foreach (GUIComponent levelObjFrame in levelObjectList.Content.Children) { var levelObj = levelObjFrame.UserData as LevelObjectPrefab; - float commonness = levelObj.GetCommonness(selectedParams); + float commonness = levelObj.GetCommonness(levelData); levelObjFrame.Color = commonness > 0.0f ? GUIStyle.Green * 0.4f : Color.Transparent; levelObjFrame.SelectedColor = commonness > 0.0f ? GUIStyle.Green * 0.6f : Color.White * 0.5f; levelObjFrame.HoverColor = commonness > 0.0f ? GUIStyle.Green * 0.7f : Color.White * 0.6f; @@ -712,7 +715,7 @@ namespace Barotrauma { var levelObj1 = c1.GUIComponent.UserData as LevelObjectPrefab; var levelObj2 = c2.GUIComponent.UserData as LevelObjectPrefab; - return Math.Sign(levelObj2.GetCommonness(selectedParams) - levelObj1.GetCommonness(selectedParams)); + return Math.Sign(levelObj2.GetCommonness(levelData) - levelObj1.GetCommonness(levelData)); }); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index ff6f3240e..001618db3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -2902,6 +2902,7 @@ namespace Barotrauma appearanceFrame.ClearChildren(); var info = GameMain.Client.CharacterInfo ?? Character.Controlled?.Info; + CharacterAppearanceCustomizationMenu?.Dispose(); CharacterAppearanceCustomizationMenu = new CharacterInfo.AppearanceCustomizationMenu(info, appearanceFrame) { OnHeadSwitch = menu => diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index aad8f3493..8a78c4e45 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -291,7 +291,7 @@ namespace Barotrauma }, style: null, color: Color.Black * 0.6f); var colorLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), colorComponentLabels[i], font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft); - var numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Int) + var numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), NumberType.Int) { Font = GUIStyle.SmallFont }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 66ea4de08..b4c90e032 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -2256,7 +2256,7 @@ namespace Barotrauma { ToolTip = TextManager.Get("OutPostModuleMaxCountToolTip") }; - new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), maxModuleCountGroup.RectTransform), GUINumberInput.NumberType.Int) + new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), maxModuleCountGroup.RectTransform), NumberType.Int) { ToolTip = TextManager.Get("OutPostModuleMaxCountToolTip"), IntValue = MainSub?.Info?.OutpostModuleInfo?.MaxCount ?? 1000, @@ -2274,7 +2274,7 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), commonnessGroup.RectTransform), TextManager.Get("subeditor.outpostcommonness"), textAlignment: Alignment.CenterLeft, wrap: true); - new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), commonnessGroup.RectTransform), GUINumberInput.NumberType.Float) + new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), commonnessGroup.RectTransform), NumberType.Float) { FloatValue = MainSub?.Info?.OutpostModuleInfo?.Commonness ?? 10, MinValueFloat = 0, @@ -2304,8 +2304,9 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), priceGroup.RectTransform), TextManager.Get("subeditor.price"), textAlignment: Alignment.CenterLeft, wrap: true); + int basePrice = (GameMain.DebugDraw ? 0 : MainSub?.CalculateBasePrice()) ?? 1000; - new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), priceGroup.RectTransform), GUINumberInput.NumberType.Int, hidePlusMinusButtons: true) + new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), priceGroup.RectTransform), NumberType.Int, hidePlusMinusButtons: true) { IntValue = Math.Max(MainSub?.Info?.Price ?? basePrice, basePrice), MinValueInt = basePrice, @@ -2350,13 +2351,13 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), crewSizeArea.RectTransform), TextManager.Get("RecommendedCrewSize"), textAlignment: Alignment.CenterLeft, wrap: true, font: GUIStyle.SmallFont); - var crewSizeMin = new GUINumberInput(new RectTransform(new Vector2(0.17f, 1.0f), crewSizeArea.RectTransform), GUINumberInput.NumberType.Int, relativeButtonAreaWidth: 0.25f) + var crewSizeMin = new GUINumberInput(new RectTransform(new Vector2(0.17f, 1.0f), crewSizeArea.RectTransform), NumberType.Int, relativeButtonAreaWidth: 0.25f) { MinValueInt = 1, MaxValueInt = 128 }; new GUITextBlock(new RectTransform(new Vector2(0.06f, 1.0f), crewSizeArea.RectTransform), "-", textAlignment: Alignment.Center); - var crewSizeMax = new GUINumberInput(new RectTransform(new Vector2(0.17f, 1.0f), crewSizeArea.RectTransform), GUINumberInput.NumberType.Int, relativeButtonAreaWidth: 0.25f) + var crewSizeMax = new GUINumberInput(new RectTransform(new Vector2(0.17f, 1.0f), crewSizeArea.RectTransform), NumberType.Int, relativeButtonAreaWidth: 0.25f) { MinValueInt = 1, MaxValueInt = 128 @@ -2947,11 +2948,22 @@ namespace Barotrauma prevSub = sub; } + string pathWithoutUserName = Path.GetFullPath(sub.FilePath); + string saveFolder = Path.GetFullPath(SaveUtil.SaveFolder); + if (pathWithoutUserName.StartsWith(saveFolder)) + { + pathWithoutUserName = "..." + pathWithoutUserName[saveFolder.Length..]; + } + else + { + pathWithoutUserName = sub.FilePath; + } + GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), subList.Content.RectTransform) { MinSize = new Point(0, 30) }, ToolBox.LimitString(sub.Name, GUIStyle.Font, subList.Rect.Width - 80)) { UserData = sub, - ToolTip = sub.FilePath + ToolTip = pathWithoutUserName }; if (!(ContentPackageManager.VanillaCorePackage?.Files.Any(f => f.Path == sub.FilePath) ?? false)) @@ -3667,17 +3679,17 @@ namespace Barotrauma GUILayoutGroup hueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.25f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), hueSliderLayout.RectTransform), text: "H:", font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Hue" }; GUIScrollBar hueScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), hueSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = currentHue }; - GUINumberInput hueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), hueSliderLayout.RectTransform), inputType: GUINumberInput.NumberType.Float) { FloatValue = currentHue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; + GUINumberInput hueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), hueSliderLayout.RectTransform), inputType: NumberType.Float) { FloatValue = currentHue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; GUILayoutGroup satSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), satSliderLayout.RectTransform), text: "S:", font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Saturation"}; GUIScrollBar satScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), satSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = colorPicker.SelectedSaturation }; - GUINumberInput satTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), satSliderLayout.RectTransform), inputType: GUINumberInput.NumberType.Float) { FloatValue = colorPicker.SelectedSaturation, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; + GUINumberInput satTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), satSliderLayout.RectTransform), inputType: NumberType.Float) { FloatValue = colorPicker.SelectedSaturation, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; GUILayoutGroup valueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), valueSliderLayout.RectTransform), text: "V:", font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Value"}; GUIScrollBar valueScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), valueSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = colorPicker.SelectedValue }; - GUINumberInput valueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), valueSliderLayout.RectTransform), inputType: GUINumberInput.NumberType.Float) { FloatValue = colorPicker.SelectedValue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; + GUINumberInput valueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), valueSliderLayout.RectTransform), inputType: NumberType.Float) { FloatValue = colorPicker.SelectedValue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; GUILayoutGroup colorInfoLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.3f), sliderLayout.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) { RelativeSpacing = 0.15f }; @@ -4439,8 +4451,7 @@ namespace Barotrauma { Rectangle hullRect = rect; hullRect.Y = -hullRect.Y; - Hull newHull = new Hull(MapEntityPrefab.FindByIdentifier("hull".ToIdentifier()), - hullRect, + Hull newHull = new Hull(hullRect, MainSub); } @@ -4452,7 +4463,7 @@ namespace Barotrauma Rectangle gapRect = e.WorldRect; gapRect.Y -= 8; gapRect.Height = 16; - Gap newGap = new Gap(MapEntityPrefab.FindByIdentifier("gap".ToIdentifier()), gapRect); + new Gap(gapRect); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index af4eba036..fb767cd11 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -59,7 +59,7 @@ namespace Barotrauma { if (field is GUINumberInput numInput) { - if (numInput.InputType == GUINumberInput.NumberType.Float) + if (numInput.InputType == NumberType.Float) { numInput.FloatValue = f; if (flash) @@ -76,7 +76,7 @@ namespace Barotrauma { if (field is GUINumberInput numInput) { - if (numInput.InputType == GUINumberInput.NumberType.Int) + if (numInput.InputType == NumberType.Int) { numInput.IntValue = integer; if (flash) @@ -127,7 +127,7 @@ namespace Barotrauma var field = fields[i]; if (field is GUINumberInput numInput) { - if (numInput.InputType == GUINumberInput.NumberType.Float) + if (numInput.InputType == NumberType.Float) { numInput.FloatValue = i == 0 ? v2.X : v2.Y; if (flash) @@ -145,7 +145,7 @@ namespace Barotrauma var field = fields[i]; if (field is GUINumberInput numInput) { - if (numInput.InputType == GUINumberInput.NumberType.Float) + if (numInput.InputType == NumberType.Float) { switch (i) { @@ -174,7 +174,7 @@ namespace Barotrauma var field = fields[i]; if (field is GUINumberInput numInput) { - if (numInput.InputType == GUINumberInput.NumberType.Float) + if (numInput.InputType == NumberType.Float) { switch (i) { @@ -206,7 +206,7 @@ namespace Barotrauma var field = fields[i]; if (field is GUINumberInput numInput) { - if (numInput.InputType == GUINumberInput.NumberType.Int) + if (numInput.InputType == NumberType.Int) { switch (i) { @@ -246,7 +246,7 @@ namespace Barotrauma var field = fields[i]; if (field is GUINumberInput numInput) { - if (numInput.InputType == GUINumberInput.NumberType.Int) + if (numInput.InputType == NumberType.Int) { switch (i) { @@ -517,7 +517,7 @@ namespace Barotrauma } else { - var numberInput = new GUINumberInput(new RectTransform(new Vector2(inputFieldWidth, 1), frame.RectTransform, Anchor.TopRight), GUINumberInput.NumberType.Int) + var numberInput = new GUINumberInput(new RectTransform(new Vector2(inputFieldWidth, 1), frame.RectTransform, Anchor.TopRight), NumberType.Int) { ToolTip = toolTip, Font = GUIStyle.SmallFont @@ -554,7 +554,7 @@ namespace Barotrauma }; GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(inputFieldWidth, 1), frame.RectTransform, - Anchor.TopRight), GUINumberInput.NumberType.Float) + Anchor.TopRight), NumberType.Float) { ToolTip = toolTip, Font = GUIStyle.SmallFont @@ -770,7 +770,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUIStyle.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), - GUINumberInput.NumberType.Int) + NumberType.Int) { Font = GUIStyle.SmallFont }; @@ -838,7 +838,7 @@ namespace Barotrauma } new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUIStyle.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), - GUINumberInput.NumberType.Float) + NumberType.Float) { Font = GUIStyle.SmallFont }; @@ -909,7 +909,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUIStyle.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), - GUINumberInput.NumberType.Float) + NumberType.Float) { Font = GUIStyle.SmallFont }; @@ -985,7 +985,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUIStyle.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), - GUINumberInput.NumberType.Float) + NumberType.Float) { Font = GUIStyle.SmallFont }; @@ -1078,7 +1078,7 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(0.2f, 1), element.RectTransform, Anchor.CenterLeft) { MinSize = new Point(15, 0) }, GUI.ColorComponentLabels[i], font: GUIStyle.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), - GUINumberInput.NumberType.Int) + NumberType.Int) { Font = GUIStyle.SmallFont }; @@ -1153,7 +1153,7 @@ namespace Barotrauma var element = new GUIFrame(new RectTransform(new Vector2(0.22f, 1), inputArea.RectTransform) { MinSize = new Point(50, 0), MaxSize = new Point(150, 50) }, style: null); new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), GUI.RectComponentLabels[i], font: GUIStyle.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), - GUINumberInput.NumberType.Int) + NumberType.Int) { Font = GUIStyle.SmallFont }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index 9a1a30d8a..d4ef4f065 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -182,7 +182,7 @@ namespace Barotrauma private void Slider(GUILayoutGroup parent, Vector2 range, int steps, Func labelFunc, float currentValue, Action setter, LocalizedString? tooltip = null) { var layout = new GUILayoutGroup(NewItemRectT(parent), isHorizontal: true); - var slider = new GUIScrollBar(new RectTransform((0.82f, 1.0f), layout.RectTransform), style: "GUISlider") + var slider = new GUIScrollBar(new RectTransform((0.72f, 1.0f), layout.RectTransform), style: "GUISlider") { Range = range, BarScrollValue = currentValue, @@ -193,7 +193,7 @@ namespace Barotrauma { slider.ToolTip = tooltip; } - var label = new GUITextBlock(new RectTransform((0.18f, 1.0f), layout.RectTransform), + var label = new GUITextBlock(new RectTransform((0.28f, 1.0f), layout.RectTransform), labelFunc(currentValue), wrap: false, textAlignment: Alignment.Center); slider.OnMoved = (sb, val) => { @@ -217,9 +217,6 @@ namespace Barotrauma }; } - private string ScaleResolution(float scale) => - $"{Round(unsavedConfig.Graphics.Width * scale)}\nx\n{Round(unsavedConfig.Graphics.Height * scale)}"; - private string Percentage(float v) => $"{Round(v * 100)}%"; private int Round(float v) => (int)MathF.Round(v); @@ -259,22 +256,27 @@ 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); - Label(right, TextManager.Get("ParticleLimit"), GUIStyle.SubHeadingFont); - Slider(right, (100, 1500), 15, (v) => Round(v).ToString(), unsavedConfig.Graphics.ParticleLimit, (v) => unsavedConfig.Graphics.ParticleLimit = Round(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); Spacer(right); - + Label(right, TextManager.Get("LightMapScale"), GUIStyle.SubHeadingFont); - Slider(right, (0.5f, 1.0f), 10, ScaleResolution, unsavedConfig.Graphics.LightMapScale, (v) => unsavedConfig.Graphics.LightMapScale = v, TextManager.Get("LightMapScaleTooltip")); + Slider(right, (0.5f, 1.0f), 11, (v) => TextManager.GetWithVariable("percentageformat", "[value]", Round(v * 100).ToString()).Value, unsavedConfig.Graphics.LightMapScale, (v) => unsavedConfig.Graphics.LightMapScale = v, TextManager.Get("LightMapScaleTooltip")); Spacer(right); - + + Label(right, TextManager.Get("VisibleLightLimit"), GUIStyle.SubHeadingFont); + Slider(right, (10, 210), 21, (v) => v > 200 ? TextManager.Get("unlimited").Value : Round(v).ToString(), unsavedConfig.Graphics.VisibleLightLimit, + (v) => unsavedConfig.Graphics.VisibleLightLimit = v > 200 ? int.MaxValue : Round(v), TextManager.Get("VisibleLightLimitTooltip")); + Spacer(right); + Tickbox(right, TextManager.Get("RadialDistortion"), TextManager.Get("RadialDistortionTooltip"), unsavedConfig.Graphics.RadialDistortion, (v) => unsavedConfig.Graphics.RadialDistortion = v); Tickbox(right, TextManager.Get("ChromaticAberration"), TextManager.Get("ChromaticAberrationTooltip"), unsavedConfig.Graphics.ChromaticAberration, (v) => unsavedConfig.Graphics.ChromaticAberration = v); + + Label(right, TextManager.Get("ParticleLimit"), GUIStyle.SubHeadingFont); + Slider(right, (100, 1500), 15, (v) => Round(v).ToString(), unsavedConfig.Graphics.ParticleLimit, (v) => unsavedConfig.Graphics.ParticleLimit = Round(v)); + Spacer(right); } - + private static string TrimAudioDeviceName(string name) { if (string.IsNullOrWhiteSpace(name)) { return string.Empty; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs index 9eab19935..14b0b68e2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs @@ -202,11 +202,6 @@ namespace Barotrauma { Sound?.Dispose(); Sound = null; } - - ~SoundPrefab() - { - Dispose(); - } } [TagNames("damagesound")] diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index fd9431cc1..7696fbefe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -91,11 +91,6 @@ namespace Barotrauma.Steam } } - ~ItemThumbnail() - { - Dispose(); - } - public void Dispose() { if (ItemId == 0) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs index fdc29524d..136290f81 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs @@ -44,12 +44,7 @@ namespace Barotrauma.Steam } }); } - - ~LocalThumbnail() - { - Dispose(); - } - + private bool disposed = false; public void Dispose() { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs index 3dade74cc..272f9e4c9 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); - Camera cam = new Camera(); + using 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 6b590be38..f691db09d 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.1.0 + 0.18.2.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma @@ -64,11 +64,18 @@ true - + + + + + + + + diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj.bak b/Barotrauma/BarotraumaClient/LinuxClient.csproj.bak deleted file mode 100644 index a2ee9a9e0..000000000 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj.bak +++ /dev/null @@ -1,208 +0,0 @@ - - - - WinExe - netcoreapp3.1 - Barotrauma - FakeFish, Undertow Games - Barotrauma - 0.18.0.0 - Copyright © FakeFish 2018-2022 - AnyCPU;x64 - Barotrauma - ..\BarotraumaShared\Icon.ico - Debug;Release;Unstable - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 - - - - - - DEBUG;TRACE;CLIENT;LINUX;USE_STEAM - x64 - ..\bin\$(Configuration)Linux\ - net6.0 - 8 - - - - TRACE;DEBUG;CLIENT;LINUX;X64;USE_STEAM - x64 - ..\bin\$(Configuration)Linux\ - net6.0 - 8 - - - - TRACE;CLIENT;LINUX;USE_STEAM - x64 - ..\bin\$(Configuration)Linux\ - - - - TRACE;CLIENT;LINUX;USE_STEAM;UNSTABLE - x64 - ..\bin\$(Configuration)Linux\ - true - - - - TRACE;CLIENT;LINUX;X64;USE_STEAM - x64 - ..\bin\$(Configuration)Linux\ - - - - TRACE;CLIENT;LINUX;X64;USE_STEAM;UNSTABLE - x64 - ..\bin\$(Configuration)Linux\ - true - - - - - - - - - - - - - - - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - - PreserveNewest - - - - Icon.bmp - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - $(IntermediateOutputPath)gitver - $(IntermediateOutputPath)gitbranch - - - - - - - - - - - - - - - - - - @(GitVersion) - - - - - - - - - @(GitBranch) - - - - - - - $(IntermediateOutputPath)CustomAssemblyInfo.cs - - - - - - - - - <_Parameter1>GitRevision - <_Parameter2>$(BuildHash) - - - <_Parameter1>GitBranch - <_Parameter2>$(BuildBranch) - - - <_Parameter1>ProjectDir - <_Parameter2>$(ProjectDir) - - - - - - - - linux-x64 - - - - \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 313a64a2f..7842466de 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.1.0 + 0.18.2.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma @@ -55,19 +55,20 @@ true - + + + + + + + + - - SharedSource\Prefabs\PrefabSelector.cs - - - SharedSource\Prefabs\PrefabCollectionSubset.cs - diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj.bak b/Barotrauma/BarotraumaClient/MacClient.csproj.bak deleted file mode 100644 index 9360c73bf..000000000 --- a/Barotrauma/BarotraumaClient/MacClient.csproj.bak +++ /dev/null @@ -1,214 +0,0 @@ - - - - WinExe - netcoreapp3.1 - Barotrauma - FakeFish, Undertow Games - Barotrauma - 0.18.0.0 - Copyright © FakeFish 2018-2022 - AnyCPU;x64 - Barotrauma - ..\BarotraumaShared\Icon.ico - Debug;Release;Unstable - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 - - - - TRACE;CLIENT;OSX;USE_STEAM;DEBUG;NETCOREAPP;NETCOREAPP3_0 - x64 - ..\bin\$(Configuration)Mac - - - - TRACE;DEBUG;CLIENT;OSX;X64;USE_STEAM - x64 - ..\bin\$(Configuration)Mac\ - - - - TRACE;CLIENT;OSX;USE_STEAM;RELEASE;NETCOREAPP;NETCOREAPP3_0 - x64 - - ..\bin\$(Configuration)Mac - - - - TRACE;CLIENT;OSX;USE_STEAM;RELEASE;NETCOREAPP;NETCOREAPP3_0;UNSTABLE - x64 - - ..\bin\$(Configuration)Mac - true - - - - TRACE;CLIENT;OSX;X64;USE_STEAM - x64 - ..\bin\$(Configuration)Mac\ - - - - TRACE;CLIENT;OSX;X64;USE_STEAM;UNSTABLE - x64 - ..\bin\$(Configuration)Mac\ - true - - - - - - - - - - - SharedSource\Prefabs\PrefabSelector.cs - - - SharedSource\Prefabs\PrefabCollectionSubset.cs - - - - - - - - - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - - Icon.bmp - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - libsteam_api64.dylib - PreserveNewest - - - - - PreserveNewest - - - - - - $(IntermediateOutputPath)gitver - $(IntermediateOutputPath)gitbranch - - - - - - - - - - - - - - - - - - @(GitVersion) - - - - - - - - - @(GitBranch) - - - - - - - $(IntermediateOutputPath)CustomAssemblyInfo.cs - - - - - - - - - <_Parameter1>GitRevision - <_Parameter2>$(BuildHash) - - - <_Parameter1>GitBranch - <_Parameter2>$(BuildBranch) - - - <_Parameter1>ProjectDir - <_Parameter2>$(ProjectDir) - - - - - - - - osx-x64 - - - - \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index b17eb41f9..fa39bbdbb 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.1.0 + 0.18.2.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma @@ -61,19 +61,20 @@ true - + + + + + + + - - SharedSource\Steam\AuthTicket.cs - - - SharedSource\Utils\Result.cs - + - + diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj.bak b/Barotrauma/BarotraumaClient/WindowsClient.csproj.bak deleted file mode 100644 index b9fecd89c..000000000 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj.bak +++ /dev/null @@ -1,237 +0,0 @@ - - - - WinExe - netcoreapp3.1 - Barotrauma - FakeFish, Undertow Games - Barotrauma - 0.18.0.0 - Copyright © FakeFish 2018-2022 - AnyCPU;x64 - Barotrauma - ..\BarotraumaShared\Icon.ico - Debug;Release;Unstable - app.manifest - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 - - - - DEBUG;TRACE;CLIENT;WINDOWS;USE_STEAM - x64 - ..\bin\$(Configuration)Windows\ - - - - TRACE;DEBUG;CLIENT;WINDOWS;X64;USE_STEAM - x64 - ..\bin\$(Configuration)Windows\ - full - true - Auto - - - - TRACE;CLIENT;WINDOWS;USE_STEAM - x64 - ..\bin\$(Configuration)Windows\ - - - - TRACE;CLIENT;WINDOWS;USE_STEAM - x64 - ..\bin\$(Configuration)Windows\ - true - - - - TRACE;CLIENT;WINDOWS;X64;USE_STEAM - x64 - ..\bin\$(Configuration)Windows\ - full - true - - - - TRACE;CLIENT;WINDOWS;X64;USE_STEAM - x64 - ..\bin\$(Configuration)Windows\ - full - true - true - - - - - - - - - SharedSource\Steam\AuthTicket.cs - - - SharedSource\Utils\Result.cs - - - - - - - - - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - - PreserveNewest - - - - - - - - - - - - - - - - - - - - - - - - - SharedSource\Utils\Result - - - - - - - - - - - - Never - - - Never - - - Never - - - Never - - - Never - - - Never - - - Never - - - Never - - - - - - - - $(IntermediateOutputPath)gitver - $(IntermediateOutputPath)gitbranch - - - - - - - - - - - - - - - - - - @(GitVersion) - - - - - - - - - @(GitBranch) - - - - - - - $(IntermediateOutputPath)CustomAssemblyInfo.cs - - - - - - - - - <_Parameter1>GitRevision - <_Parameter2>$(BuildHash) - - - <_Parameter1>GitBranch - <_Parameter2>$(BuildBranch) - - - <_Parameter1>ProjectDir - <_Parameter2>$(ProjectDir) - - - - - - - - win-x64 - - - - \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 2eedc6301..dec7d455e 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.1.0 + 0.18.2.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer @@ -65,10 +65,11 @@ - + + diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj.bak b/Barotrauma/BarotraumaServer/LinuxServer.csproj.bak deleted file mode 100644 index ab1e78f08..000000000 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj.bak +++ /dev/null @@ -1,156 +0,0 @@ - - - - Exe - netcoreapp3.1 - Barotrauma - FakeFish, Undertow Games - Barotrauma Dedicated Server - 0.18.0.0 - Copyright © FakeFish 2018-2022 - AnyCPU;x64 - DedicatedServer - ..\BarotraumaShared\Icon.ico - Debug;Release;Unstable - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 - - - - - - DEBUG;TRACE;SERVER;LINUX;USE_STEAM - x64 - ..\bin\$(Configuration)Linux\ - net6.0 - 8 - - - - TRACE;DEBUG;SERVER;LINUX;X64;USE_STEAM - x64 - ..\bin\$(Configuration)Linux\ - net6.0 - 8 - - - - TRACE;SERVER;LINUX;USE_STEAM - x64 - ..\bin\$(Configuration)Linux\ - - - - TRACE;SERVER;LINUX;USE_STEAM - x64 - ..\bin\$(Configuration)Linux\ - true - - - - TRACE;SERVER;LINUX;X64;USE_STEAM - x64 - ..\bin\$(Configuration)Linux\ - - - - TRACE;SERVER;LINUX;X64;USE_STEAM - x64 - ..\bin\$(Configuration)Linux\ - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - $(IntermediateOutputPath)gitver - $(IntermediateOutputPath)gitbranch - - - - - - - - - - - - - - - - - - @(GitVersion) - - - - - - - - - @(GitBranch) - - - - - - - $(IntermediateOutputPath)CustomAssemblyInfo.cs - - - - - - - - - <_Parameter1>GitRevision - <_Parameter2>$(BuildHash) - - - <_Parameter1>GitBranch - <_Parameter2>$(BuildBranch) - - - <_Parameter1>ProjectDir - <_Parameter2>$(ProjectDir) - - - - - - - diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 41800c1ca..6d151701f 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.1.0 + 0.18.2.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer @@ -58,10 +58,11 @@ - + + diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj.bak b/Barotrauma/BarotraumaServer/MacServer.csproj.bak deleted file mode 100644 index 105be0054..000000000 --- a/Barotrauma/BarotraumaServer/MacServer.csproj.bak +++ /dev/null @@ -1,157 +0,0 @@ - - - - Exe - netcoreapp3.1 - Barotrauma - FakeFish, Undertow Games - Barotrauma Dedicated Server - 0.18.0.0 - Copyright © FakeFish 2018-2022 - AnyCPU;x64 - DedicatedServer - ..\BarotraumaShared\Icon.ico - Debug;Release;Unstable - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 - - - - TRACE;SERVER;OSX;USE_STEAM;DEBUG;NETCOREAPP;NETCOREAPP3_0 - x64 - ..\bin\DebugMac - true - - - - - TRACE;DEBUG;SERVER;OSX;X64;USE_STEAM - x64 - ..\bin\$(Configuration)Mac\ - - - - TRACE;SERVER;OSX;USE_STEAM;RELEASE;NETCOREAPP;NETCOREAPP3_0 - x64 - - ..\bin\ReleaseMac - - - - TRACE;SERVER;OSX;USE_STEAM;RELEASE;NETCOREAPP;NETCOREAPP3_0;UNSTABLE - x64 - - ..\bin\ReleaseMac - true - - - - TRACE;SERVER;OSX;X64;USE_STEAM - x64 - ..\bin\$(Configuration)Mac\ - - - - TRACE;SERVER;OSX;X64;USE_STEAM;UNSTABLE - x64 - ..\bin\$(Configuration)Mac\ - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - libsteam_api64.dylib - PreserveNewest - - - - - - $(IntermediateOutputPath)gitver - $(IntermediateOutputPath)gitbranch - - - - - - - - - - - - - - - - - - @(GitVersion) - - - - - - - - - @(GitBranch) - - - - - - - $(IntermediateOutputPath)CustomAssemblyInfo.cs - - - - - - - - - <_Parameter1>GitRevision - <_Parameter2>$(BuildHash) - - - <_Parameter1>GitBranch - <_Parameter2>$(BuildBranch) - - - <_Parameter1>ProjectDir - <_Parameter2>$(ProjectDir) - - - - - - - diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs index 7c2bc102a..c1742cfa7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs @@ -26,26 +26,34 @@ namespace Barotrauma.Items.Components { for (int i = 0; i < customInterfaceElementList.Count; i++) { - if (customInterfaceElementList[i].HasPropertyName) + var element = customInterfaceElementList[i]; + if (element.HasPropertyName) { - if (!customInterfaceElementList[i].IsIntegerInput) + if (!element.IsNumberInput) { - TextChanged(customInterfaceElementList[i], elementValues[i]); + TextChanged(element, elementValues[i]); } else { - int.TryParse(elementValues[i], out int value); - ValueChanged(customInterfaceElementList[i], value); + switch (element.NumberType) + { + case NumberType.Int when int.TryParse(elementValues[i], out int value): + ValueChanged(element, value); + break; + case NumberType.Float when TryParseFloatInvariantCulture(elementValues[i], out float value): + ValueChanged(element, value); + break; + } } } - else if (customInterfaceElementList[i].ContinuousSignal) + else if (element.ContinuousSignal) { - TickBoxToggled(customInterfaceElementList[i], elementStates[i]); + TickBoxToggled(element, elementStates[i]); } else if (elementStates[i]) { - clickedButton = customInterfaceElementList[i]; - ButtonClicked(customInterfaceElementList[i]); + clickedButton = element; + ButtonClicked(element); } } } @@ -61,13 +69,14 @@ namespace Barotrauma.Items.Components //extradata contains an array of buttons clicked by a client (or nothing if nothing was clicked) for (int i = 0; i < customInterfaceElementList.Count; i++) { - if (customInterfaceElementList[i].HasPropertyName) + var element = customInterfaceElementList[i]; + if (element.HasPropertyName) { - msg.Write(customInterfaceElementList[i].Signal); + msg.Write(element.Signal); } - else if(customInterfaceElementList[i].ContinuousSignal) + else if(element.ContinuousSignal) { - msg.Write(customInterfaceElementList[i].State); + msg.Write(element.State); } else { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index 12fa0d853..13b9156e4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -208,7 +208,8 @@ namespace Barotrauma // Do not take unanswered into account for total int yes = GameMain.Server.ConnectedClients.Count(c => c.InGame && c.GetVote(ActiveVote.VoteType) == 2); int no = GameMain.Server.ConnectedClients.Count(c => c.InGame && c.GetVote(ActiveVote.VoteType) == 1); - ActiveVote.Finish(this, passed: yes / (float)(yes + no) >= GameMain.NetworkMember.ServerSettings.VoteRequiredRatio); + int total = Math.Max(yes + no, 1); + ActiveVote.Finish(this, passed: yes / (float)(total) >= GameMain.NetworkMember.ServerSettings.VoteRequiredRatio); } } diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index d83075d47..a1886f6b2 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.1.0 + 0.18.2.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer @@ -60,10 +60,11 @@ - + + diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj.bak b/Barotrauma/BarotraumaServer/WindowsServer.csproj.bak deleted file mode 100644 index 0fbb606a7..000000000 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj.bak +++ /dev/null @@ -1,152 +0,0 @@ - - - - Exe - netcoreapp3.1 - Barotrauma - FakeFish, Undertow Games - Barotrauma Dedicated Server - 0.18.0.0 - Copyright © FakeFish 2018-2022 - AnyCPU;x64 - DedicatedServer - ..\BarotraumaShared\Icon.ico - Debug;Release;Unstable - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 - - - - DEBUG;TRACE;SERVER;WINDOWS;USE_STEAM - x64 - ..\bin\$(Configuration)Windows\ - - - - TRACE;DEBUG;SERVER;WINDOWS;X64;USE_STEAM - x64 - ..\bin\$(Configuration)Windows\ - full - true - - - - TRACE;SERVER;WINDOWS;USE_STEAM - x64 - ..\bin\$(Configuration)Windows\ - - - - TRACE;SERVER;WINDOWS;USE_STEAM - x64 - ..\bin\$(Configuration)Windows\ - true - - - - TRACE;SERVER;WINDOWS;X64;USE_STEAM - x64 - ..\bin\$(Configuration)Windows\ - full - true - - - - TRACE;SERVER;WINDOWS;X64;USE_STEAM - x64 - ..\bin\$(Configuration)Windows\ - full - true - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - $(IntermediateOutputPath)gitver - $(IntermediateOutputPath)gitbranch - - - - - - - - - - - - - - - - - - @(GitVersion) - - - - - - - - - @(GitBranch) - - - - - - - $(IntermediateOutputPath)CustomAssemblyInfo.cs - - - - - - - - - <_Parameter1>GitRevision - <_Parameter2>$(BuildHash) - - - <_Parameter1>GitBranch - <_Parameter2>$(BuildBranch) - - - <_Parameter1>ProjectDir - <_Parameter2>$(ProjectDir) - - - - - - - diff --git a/Barotrauma/BarotraumaShared/Data/Saves/TheColdsBelow.save b/Barotrauma/BarotraumaShared/Data/Saves/TheColdsBelow.save new file mode 100644 index 0000000000000000000000000000000000000000..f6a00dd10c46450fe545d1abe890c37f4e921e56 GIT binary patch literal 322104 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

I4l%f!%6#ty*uq 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%0DAKwo|P3XtV;?YL9dQHye?bY389X@ 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 literal 0 HcmV?d00001 diff --git a/Barotrauma/BarotraumaShared/Data/Saves/Zapisz_1.save b/Barotrauma/BarotraumaShared/Data/Saves/Zapisz_1.save new file mode 100644 index 0000000000000000000000000000000000000000..f892d03747ac68e75ec44c029ece1070c43a9de7 GIT binary patch literal 240323 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

dAr2w~Fjc^3seQDhjspIRQ-$0;VY zf16DNkHrbV$Wa7a@jy7vr2o#Kk}9N6ANo^Ev}0x}2f5e$`u$sA+r2)@l_#AyL#jSu zh-$aOfh9mJ()LRQO3=I7Da4gBc~y&i*`CXLPD6FDm20BK1D}w6`%aJgZ!SGpLrR&y zrS0=|V{XVsLZ#TUk~>FtShIS{m+^)d`kArpT)hu;-WN;7w(Cp3WhL?+to176qu4tU zv$yJS4R_X1Ri&oz^SLroqLEwLE=PfliUqD(h}kLo4W1~n+vs%bN9fI_+7c~Qt26st zmAvmAU1O4lHkQv`42#!P{f0ZU7tMWA6q!DWI9BpjS{xC@6=Fmy* zHkbKN44SIwmA%oha{B@7%@u;cfe`^{oKJT8hzqpGy3i#`@etnMn;#Gc_v@PdD(MC3nEH)2%f;D0OD z2sFl;A-oafy-#%-2U8O*IQDkERVN%q`i6MM|AJ#MR%Q_;_T!q-vR&9N#D+lj;{F2TG9{++J95P zOiNa9GZ?l+v9RC;1Nx1Z$DJ6~M1(ClIWxH@$;rlB$d3x$USm|3@{(ta?W7FR(&M!AM zy#AXtsxUDHetumdt=zSAOo$GA>T%z=~Ts%Wiju&WM_mripWomv( zWs|pz!1GI}5Vrwa95VHF<8-Ht)^{J?e6>2$kW_UOpD;rjWL1B>nZmWr_5&>1(D9R< zcp81DA=K-mgc4|wAVYgY>!CqD0Q$=C@4p>d27Py1sO*V2Bb7K@L+HG^X7KcWY?(C#k7mB))0DCG6Kgpp zQ5Sb&d7EmrnqdrO9bwIBB`vZ*lAS(9p94uEr$P8N9$d>iZS-c$l1Xa)g@|IKp|jpe zbRQ*TL}gVIwOK`_jpT9Z*t;Y(%?ddHok7Vax-wWo{@Zw>x#@ZM?NB`DJwG5s{!ba9 zaHW=@ro7@|X!YW>`at4hw+R(Q{R1n^`6V2kLFjCIU`1SoNO_ezgZ{3zUa7Jmo1PxI ztX4%71#4v<;-I>>i4{RbMUM1Dh;wjm8HfWe5ZDJ$Gh_oRr*9Zq&6E#nu`fc z(f?O=O(*qz7jtRx9nh@n^ley;`-nwtf#M&}UHUFMMD#Wm;=SG4g&T-6q@{)8KL#*^ z;;UPNQ$b^F+qC)97J>__m9tRO3wtsV9`3QcERX==WCi7o^0xpRM1D^oMd6FKhgY8d z*n1xwzy+Bj03{71pf+#-l0l2hhWp1CRv1iFtNkQ8fgINdv!&@z;CNKSb!>1;hzY+o z)>g=J#-^Xe4(h*LMY;S;U{YG_7FKVdur~AVa91Fs$hDD)S@8}V551J@oj6Ln2A9Se zsg{Q+_hsMd{AZpGHm_PEN6jKs0gthUgz=#s|z6IpuEB|I9Y-)>9_>NuBlXB~Gh z3vCa|jStF|yKf!cIy+CdM=ts{T0hGIsdI!nYEPocOAcfqEw^}_&6tUU+80+!A(n z31}?$V6*o8NJ_oTR4)3tWCj6SiU>sSBhSHa02@B<6Nb<4Pd|QdarwXAS|2^4T)V=) zMeZ1~veqW)8WzKa=|_g%`k$oC5Av2BGH!!iCwzpnhh zLU#dQtf}}T#(s?+jxy)JGJ}VZa1;&~9-wb<7aup?b+1Pd8C&dZpt!T*6rO z#L*FAh@zXstlD)DS;N%|9pp}&nj)clFo9d;UeXz6%QT>u7vVKeL_lwcXfcDxT$x%3 zQkY*ch-z$V z!!azEsRNHyS(HqZ6BK+4lYKC6{bvwCkcEPtFk!na>-(Z&zy|G=Lw$wJ;b*14RJ%9R zi^kGwfXJBOfyPVNw)3+3)B652+ZX_d%^u1Aml~T?3VAoa6W_QMj4!3kk?5d@>fX={ zhPJt?yKHjd0F{})KGlkewD^D-90jBRl77d0-d60~ux@2_rCUz=%s!$&u3zjS*Aww8 z*b%_W852jNsC6R)f&X4)a8;>=39J_I6G)t%yrreVUk;t!8ss$T;Hrp1INh`EnL`C= zA=1bhca@4b=wlkev7RBW&!^@}_Y&6rSPX*g9p3{!#8ae1qyE~#7t77bp7Qk#(d>iR z6aJ}-lVq{^S~!7ub#Ts$%=-IKEXR{RFu(>*dmHL4l{2(|1GDBd^=|`TE4yAI+A zdP1@41?b&quGHoN^{EwyiqucLe*ilvczm*+(H&R!md{)ReW_#?2J-PdJ0R$Lk~qkL z_-`w*a0s5Q^dz%bodW&3Wr^ENu&{Q3l-6gdig5MNKsL5r>y$%lwrCl>!DbZrtsCAEu-e8x)*03ZcWb)mUV~BR$Gf6 zg!qDp8Mlk$S(o7=5Sh3`tB|K8%ZTkB5 zgPe(>$+UCQ$03k8x*IQGy(-{QV`Oip3>f@rI0)E&TF{u`5k%-EGv3O5S|y z_Tp*9LA``mufz0t2GQU1Lw_vn=5xRkYXQIM=7Cz&jey|Xr_;QKC&M#kPgX4DK&Q%* zqdp%!(^cL}v-L`R2D*()hyfw>IUvr`n#wu4 zRCrX3;HyL6i}^=Kq}e)F>)-p4!QZ0D<3O-o$k-*M3E!#rYo+IlZD|kHzoov$=07SX zIVJ>=TzZFGQ$a*NJd;t$QbtH-!I`u~&=BOR1zTT(x3#%2evsVc49Ba#AWx#514xCq zYMz~!t>Ya&v$7WGcKClfr2hNUKas@{jCj!O|BFz4AJ_-RsZgzhPM!E|00a>Dx5zk# zVpGx9kf^85M*@ z^3!KchXLz({?VOg*|rrygx#18`A`gmj_d(aJto0DBIh}RJBu<~cYD6li`2(R;o z$;pHEQXM;9I2{yS4ish*-ZX&zSff21e22x5Cs&98qlt&QcTJ4`&a>g{h#gIe2XR15 zXJZXQX8K+o?Du}@4G>DiR0-@0nVvGXMI>qz%QG}Lb`tLoHiXI-*q(i4`-0S-uB5KYiJKTb!eMBCbD+MWo15jzGi$9x^{%StF*(j zKbXhedfy?G_kdgKg#zQ1?R(7nL;8-yO_f7_6X~Y=w-X^QAA|64Tu26;Q$(>@mfM1t z35oX&e;t#g3eJprtPQ;4cYZbR%!H(kE6{?T3T?L^O-0@+4GsR}is8u_iSwdAx=7&> z+K317RAj=s_my2}D4YC(h89hFZOXJ#o!WfqN#yPXRpms~G|RtFbk=h&=J|1y8C*xt z7Bx0?r!10&sjCsf9n3%mTss>P9skaGK=!W zbtn~42T0`5>kL_B2Gc+Y<0JGE{bxr()hew0E$nxgAE`}N z^m>hn<&68Ndt{g4Ktb9YIN_ukt)X2tmJY;0p?mr}b6Y&90I%;;_AeSi&tU!$lf{~A zg1kVUKnYPo4^Fg4*%^~<2(><1$CKZn-Ojq@r3#m$RA}SG)@rb%Bm*D9I3K`MgO~M3 zjn_#t32lU+!HP?#TUu6k#3A3gY9m82&()HG=Kn|9nb1O|cir2;tgC6`Y%o;b@|fz* zbzJu7_>ve&?RW)vPohz=q?T%SA3%~of*k&(oPM_{E>B}pUL-|3HTfCg-c(-1xM%sg zj=LwFIW*xCx}3P@%2s=^8u@%}YeTEPp^xchz2#IpmCLA}hCcDcwMFkf%u~%RlNm)I@3V(yE7Hwvw z5MLEhMPV}40=jJCSCAhK5Z%zA{>Dp|hHfa(*$XJB0gjB${?k8BPE*iVINR`> zn^I;nQJtB8maK`V&IpsAV27;K0?Y^XIX^EcXczPF1NF7pWh#EC? zxx%So7&*M7|B1ZlZ-zEZx}N!a}{VRceVGArYT zcf^#IR2y^Xb}KECA-@GLgHPFKf|cpoe+1gHia+XZ7@&8=srycI%G@PE<4=yd@Nl`v z7`B8~G(P@P-xTE(9JegLAM(N>ED`_?A=~gUf++ow-l!xDHn0=k_H_qJNKy8!$}O*c zbNy8E&kL04#!u@t$$aBlU4E?I*!!D<_^I2p{b>|i-e!PxD&AOjBryv4R+kTWXB_IK zQ4hp-<)M-4Wg-5o{a_6V&fED|c323)*FP2YrW#_UOKzS*LCqVg)pnYQRt@#7qmZTc;$*tW_Z=MKYwc4PIdDiDX=|%EkapdcOP&6 zrMdRe6|hEn@{+w4iu;-nO|VPyTv(a+HCIA~Kr)Z;qNVA2E+IdHoh8Xg+F_&pDTeMDNemN=vqc|!lCN~YS?2Q(%A(kUj?L7fByCbu znIxQLq3C#gqZ7z@%@Cpm6<2ncI`+oUvfcdR!qDY4=oyteY{VIr-;NSCyBkO6uo_p* zn?hT(_7J;Yt&oxqOOa|JFRe5JD|HmGRbo)3oQGa~)S1_lC-%lz*%|mkYT$1Je z-I#Xj(%zwkwefo>?X&+#hvl8EQUCuhncIs09ltZuKb;QW@Z;ycP}en0ffrk$*uh6s zo+CkC12PKw+av8YvxUF{OTG{&Y50oZ#pwI4DYji&YD;5`F`IYra`T?t^jh;QX>^rj z>2~dbd^nas8fK)L{q@!4H)d-uz!v!{slg6g)uh1Mu5FkzIR6-~vn0R}8hx0e{ji<^ z{!JO!KCm2Rjr0G{gRAyt87^wNNyDT-(4nK_-xNP@cZ=iqDY2d!pJ z$MI;ooUT3dU{Q9utMxoW0Vz@MOcM_r>^qIqt@0{ObMn>gm#{bG`FE zn@t48d8%cAqBh&C9cSuNR}M#!4|WkR{BCe(+rZEmFsf zNe9eW2fenUki}VePyyYEr8RL?jZGuy9?XUngU?%w*V`tn7j+RK!EmALKOmHps*@^n z!fY=v#l@$LyU?Jx-`?6v8d;%xI+^|8RESb|YFwZTJ5dAN$qorkj=5)rsh7X7zCc=B(_i#R#700<~Omc&JTswg36)MT{sGkkG*$3yWgA19r0H3H(pD%!~M zQ;9@SCPK?}r5yWhXj$CSe|k~UV&yT}O~)|Iz*f4Sff~2E+G(d_dzFLpCjnn#tgb4v4 zU{y4V=Q4ZgtFg2z6)m6XE<+sk`XVX2%CQlB8<%#Cb=LWL1 zWzCBCQ|EUU#LVGC_7SsEW^`?i90t(!HMdc0T2m?2maB#$?96+3BU&|a48v5`KcgCu zUZbkE^HYhkO%Oyox7z*aQG8^wxpk6T68{FRosA(&9PgCKqD!1(dMHbUnO&`Vma6BC zW;422TpGR1ss%XXceJbX=4HoH(oCZT_CQ3TQdr|)`Vw`IES>|7_Ls5x3fbC3{Dmmg z^lByk_5WdX+B@pF$TkRJut1z#;C}07_N*~lu}EC{yJRE{&N=UM6+Dq)G$mb5#8RoS zyw;@F_I=#1zgnPpX{1xCRm`h!W6ta-CsjDBfv&~jdKxVoDfHgI1zg4nHX#tW6l%Kb zntwLl#YNi~9l9CLES=A5#L(aA3CB?fvhL1Q;PJ77-2?l~CEd8zd@`4~+1omHv5!#x zAy%zB4!_Nq!Jj}94x$^#%}Up7yyWCmeg(6hMTwJe!GehTdD>iy_Tr5|FC)mBor*~f_17V6pbwY*w6@$cE8<4C6YIy8=Pp!D z)-((7{NoBJl{ZKo1dipWQgb9*elbdhih-|k0`}!30zR&xw2le${?syH{#C8B8FZ7> zekzBFr%lf{&E_YpetQq6HEeF=DH-bf1)mR@luL;6d;q_O!B{LxWn&A}+>j$HTp{!E zwE=KkQ(?UJF;8kFOVwkqe&|LO#Ux9L7j(t-b9t~wc1fm>nv#p(b}XNFzK_m9t}5R( zAK&6>+p#-0mZq`2Rob+TiFS$`74x-8zxja{*1+<$=_(U5pjua*xu_BJsJMeWBJu zr2)ul8|R(tE1?stC8KF19BW2D$-ZoqK1o9NZfCxY0GBAp>|L<04+ATmYxvbW&ALfq zWX(zJ8^%1^LHo+eC#OM#&@}x7Y5gZtJlsE2Ck*zT+G(zjD$I_Zn*Fis1V;$gyD(sv zo8Zkp5q1<7R0G>c2FA=$Z(@yo)w%Z$+o9X^A35Ru0%B6o<&bFz&bBNvgANOFpWVUU zd!pA_fI#OroE&)fAg41VO@f!G6-Kn4{5)Dg`kTeC8|Z@b397wukZ8DzuRseUBxIv0 zcDMh3jXA&_!{2!5yMq#mrfr~E2Af1_+62eV3$h2CO-Dxq@WU338wAvsHUm2(al=A& zthX)3Rj6abxS7LvD9UcEd@hs3PXvZ3L-1RrUj1YUnkMQ9Jy*Lt{k6ZCRLXvuV(Oob z3_vAnZ*RwUhOrK|-zuA_e+BEBm=n;|?ssm#hp9Vu#3T*!QX&F5@;9SBngoe(?A!mZ zO=mDbAe7I9X>qEN^1S7snRq{=ZqRb^^pe%4CE*zre=GhB6MrWFD6Z!G(D0(9+Lgr%bJop9fdqu zlXw=NSODRrE>xG5ia0T?*GTIw$kE|)5Bh_|HW&^U97JEA3Q_d>0LNtlL=CfJ#J#&fa=#rU^_1a*u`GV41GR|le%pt7(efL@ z^=n*NH^Oa1p~Uqqgk{{ACBd*EMAL%3(*FUDKykm~5t55Fr?fBMXi5RS)5WH;+4;H2 zP(9liO~yKT%jUpbHdVHz0^?r|?IPbntIe3UId~GDEGDyUyfqwwqs~(k1|L-5!X9jY z*l=imvLX>|ofQt2zYW-QhHA%Hml&H4%v@5GKj=(H%MZWI#Ho;aEsm6cGbBt*fvkEG zDA}v8FdkNNMO7!LilY*WI1t=|vI|eL(APSJQ${6+fw0E9QGteAlH?!Zp!52VD)dS3 zCWKn2pK=MSmGK%Pp%&@Vf;&*BcP`AaB?%{7{NzEA6~)7;taOfWp77}a3j7d73Wow4 zp3yM0Lo0ys*fJx9z2Ck}25}Uu?4j68oSt=E#T%2w{}N8gJaKw*ZSG)I*u#);SgfPA zHlnSn++z<*3Q=HR_vzGYQ z*I5^}V}4F_PaW&fixQpsc}{H#f*6*-AtR}=Eu@Pwe8P%Kc3YiaV#&c$nVC&~zEUkq z#$rct^^2GcIBrTnk!E!IMO}mdcJek@d?a~ZB3mX`(rrmXJL-lY@IuuUtN*a3DT*ER=$?MP-er%YX5 zOa9$hGM(+C?mYGIP z80E>aXuK9%#wFJ5atD{YNEv>o1HGn5kF6N9wHB=Yn54a!_aEDyqJnHgIo|{a)ttOw z+fK{$@L`*5!b2DW^FlY-Cfh0h`KVS&VRE~%w&|Q`tlfLzoJ+e|@<13pIkf(pw6e-W2KsP(3yk)KlQ|eZ zAC3PaU=AqV9>Oq6$Xkd%qyJNSUH_*z1c)V=0aVC9w-TNRW03(Q9){b6;E4#(B%~`+ z`c1eZf%ssI--F{*0l)@CdV=uzT0Fcoav+B>h!KMTx=f7yLQz*jZ1L5tc~JNqNDle- zt#;(>0WGr$gAL(~L^t4eH7qi(A!aasz$D|Gd8AMdZ z<(;=sb8G1@S=m$i3mw)-e}QTagn5knFdjbVOV}C~P}nlMy$(>%BmIk|&Dx@;Y*qrZ zW&t%@oqBa_R1I|JbjhN1?GmIm^L`y%1IyGJK>pJxG};WTd1o-SW}^}_G^n1cL6uKU z7@1_JUbX=hH%%xxPq);x38dWUl(+0~6q9yOhvKS%G(|BL4t{QcY}4F|sOEZx^X!*? zZ-(=?@P&tM+aV=Vy@z=Rn+7s&RBGEpp4;FW4f|ok%G{tMGpNdcO$TJ*Q(@EGPvZ3o z8MneFr8n@>REYJ%-j1j{BBn>&v|4FIZG^qFV{RU zgg;8wao&`XSkS6DwLM7rnYU$uF*?3#1Utsk07zI0&Jo1wKEFay9tDB`bRoA{qNp|y5bq}9>pz0nr6YKf(X~uGOlXQ?yGGnqJ zuHC~tm$-JVKsbQk$BB#>AY4wnK-8|qY?KzeJu|_mY6wdZXW5B)Tp$BONV$>= z2q@SS5Q$d`TGu+tDXH%Y0Cq)gg6)O~0~jNsXvtX$-Z;SI#jQi+N)ad{5hX@L6mvlK zK~DIDm@5r&6t)|JL?KM~1SJtejFgLI=p_?0!qyOze=xv5Or6351b~Z1ZYT^7-1v5^?C>85WfZQ=m1WC!%!If(e(H+oK71gc)ga|-FC@h(S>e-+IHDW;k zjMXitYbsd23Z5cH)4`=H9NHoL`2?Vb!bChsUks<3fr4NVaZ;LC_1bwx_3m`Ry z@%B)TtAZwn68AL1VUQq;Qm_al6v~Or7>7@->!c7&z=S|lM)H^{7_8ogFfRcvFskyR z5u}n5ffYP>OfU)*kpOi(&TsFhXl7p+F<{iWIgNPpROH@*ho z>-~92DbuyYD9jNUxg`-ikL`GnXPaEKA$CML*fAaPZ5K~9({Y9Q15<;jl)*&dOu=np z?{U!JvS18gAJ01r!x}~C!Yt9N8_b%YG!!}6WECFGKY!FiR@#kC%tQkFSFrj`-8 z5z0I&Xn;^_Q8p{)5iBJ`t3@gl1m~-AgXJn1H}Dh%Df(i+S1Au&w)v5=jK#}7;Fu{T zLd%}k?u_9#KfaN|Y#Zww!&@<&^GTMqc0vrt0wG!=L{Eg;41$k!e9ub|jxsL`@vC15 zX&VQGv_T#-Z5Ava8PMoX7x#qEtcZ$8q`3w4f+yHDY3)|3cN8ciD@xtg zd~aoBoT`nCb1ysEQ30T-aWcr#$#^&pLX?h^G*(kUQT>(P)!3eHdROCi&*)u^y&IR_ z)wp*x7Vb~k^j7a`ED7xPuEt!pvv)Oi)lBbd+z~Pm1fzOa<7$+;cQvl0u6kGF-qrXx zOZ2YBy{mCo(7AUtb{_zGS7UFQqIWfJWL5+_w%*ma#5KqcR!yFA_k??- z!_t8xz`fH6th;$G*3*wQb9UoZQ)4)?VMTw(6VxvA(c!8KpD2Ks~z$;zRz8YUAz8e=KId>}Z&~ zuCWfW>3BGZO*)F=(Rh&OBQv=b338OEIgleS2eSU|@Rr>D@_iMOTgTfKG`z>M!idjn|?$!-$B6e+37!)e49JkM9mZEN?F0Xz` z$D!=m(WWB_>Tiz(#NB<5}7W71FlVsC6E|B(1WU+tT^`G$)P2 za@FLt8!@kv^2+mv{UJo8v`Ft0ufCrRo&~lCqH=# z>X8Blk|&Dwccx35<%K$H-9}Gxa#p&&H%5|V1_NY1P!$Z^X;tio4mo#14%p?!gjPt2 zS%C{EkNIn|T~ca+0Gg`;bl2D7iDr@W3Xz4gC0Cn!+oR#b^L+NG9v119Y_;XyynrTA z>Axd3lSiEA_nWCoS04ovw7{IO6>hm@%h1~sfI@P*t4-NSZ~|_9XPkyJv30>j1mQU( z1R=4XmL=$YBl^yN$LYAklsUy>{blN5y}OG!%YG-_AVSfU1f$H@dj7cJtqK@7g%2Pt zGm^Sa0_JGac|xoZ6WP)ZTQRo0!)=r;@8D^%OBwo@ z(=a+ExW!xe8iY)w>+1=-+asKnc-D6+^8hofs88P#RMHq}SgIKv&uENzA=YZB-98dPelHUS+V`WaXxQ(yYZTFSp#!p3sew76^ zuf965R_>6B${k?F%NXq)^7IxvMPJ{&7V%t0NZx?2xY!^1ZTGOCqO}PQ*u=7_MZ5e{ z*N}80*auxfcvo9B3w)Pldp5w|*TLED+^tj#v~~>4vt)17SZ~8`*AB?z`yH#@(?UUi zD2d#kBxpOm@`qg90~fXL6_Yk_`mhH54*4cKDft~q4iNc_Hr_o-c`hhg99hyjfZZ);DaPx78>gN`pN(Wb&Er*e7qp8=% zNod;V^YaH2;McCUw8^hckcw zcW+ZVip-Xqx|{J|Dd_sIe|~)_zO)LkuExn|k`BhB;W&zo8H|P_%Vxg~p+C`^8jrn% z{?^wYIQ;|?D^?UVAm9gO2xi5OCHAM;V3MTiI1j<>Pe&8$kbI}<>&3AP9Dd=M05#T> zx!pIZPCmYK{`HwEedq0(jX(1pb37gnl5{eP;*5G|nvcgf>zJ{+V@|vsv+?b^ZkZ2; z)(Sj8pG@*edi_2*xk{h>GyQn8JMOQYd*UDG_wPl$AJ#6heJ_vW(I6YgG$+P`ah7GH zTXnsms^_iN%U|ZNv>Mvyu7^JkomXS$)#TWB)1kNT)@tt1b1m!7LT}fE@cU}HM7&k} zC?90`*bIZrsMIWV%yb}aod25^{!@23{DbW%agq&&W-_AHGan8nd3LkK(~e%<9F@l2 zc0PRap@k^{IDIJS)9v8qRB#5y3`d#O=efLTvr(QkP62_Ci+=guHfDaPecT zVK~0uZH;04%93hu1qL7j@PP_A90WM(jbSbY6ZOV0?K^mujzo27R^xQu}@HI45OqLrBpfWO8}^HV;cUDR5>C#>+mo! z*U(wTeFbDkEnJKf^a{YdesHaNK)e+)K!UtFwB8uf7~n~Tq`RL;)x!a$X|WD8s=zGt z8GzWKDr*x^N*gqaQ8*c3&TtB$aZnRj>?ERfIl?4_ad$Gg|AEq`PURMen-Jid!mwWm zVhshF^NIA0FEUk8h>I)CBQX7K)V2rnc|=_aF-JPgheP2?2wUm_K7(>cC`f%>i@(64YxfgO1vPYaz5 z3*fK;8UoLtps|U<83grz1|2vxo{(Z^E5v*g^RvKgPdh!(WNEW{ zW8ZnpIo3j*heEYnWe4gn)IL?=wiQ7Dvo^@pp%1cG$~eZz)p;`kyY^2M;Gn~Li(z}d zerDPJnWnFT-h2PAzueDfFWWE2i!9wX8|IV2C>urbq-4U6({G=b9Urb*W^UqT<{qo( zEE`Ztrc=y;rRJL9U_6S)={SgHz<4rlY#})qPo?E`;AtSGiZOWgsO*>df_C=Hm(A|} zwXmb_Yq5T;MA*a8hSf>p`~$hj01o{V(UzeUH)B;|U?&j+I5ZfAjJA>H>c^o{^;c(3 zw-f;rmE^raT{lc{T!7P6L*#IdY$T5})@-+4t_rKJ+&V0U1!l z#tq0;*T!zhh1@}u2QGttb6iLg3Kt?(`9?q5W+j1$6rsGWCOudx|3%}!T@CL185eMo{Fo) zAeI;n^3>{?R!&N1oCEtEUtPG{m<@ft=F#k15brpyHJWoRtOBWVjrh z#ZL1obe3>X0rC(I&-QSRVr`7r`1%+zR<5Ff5o32|vbwHA$zlXbmIu?-s6faJ=*Hfa zq7kQE07J0PgCjc`XxLw&?P!^672Kz=@xna|uOfMOb>NpW#&nAJZr%)(<{R;*#)(Y!yLgl%oM_54pDdSj)Ofd*;xGe`ryq11meJ2RhObY z`2=3`c+J}6eVuPl(Lb$fRx^?o9?9s*mR?8Z18|I}`DE1lP;VH)4@hYa?Y9D^>J1~l zbk1G9VZ?{zov}1>JF&{I@X%hbH;lM;#wz*XR1})~X{S$;tE5kQ!w3O6yElwLf|u8b zo+*M`P^1cu;oKkq;6Ap7J&B@czVqI*kJIV|wF)E;Xs&sByt`MgyY) z5i#GI$w7z-zSwMv-D10*KQ3kgEN287CjVYK0MzwM_SeqZCBLV|;ECcCNA;5PzpKI} z+x4!dTauV`I2cV#5+%dQAep3-$*qJ-;v82lu`fa!iqxw{a{lUr9?5}SzMfAltC&=j~YUQe}A;OT?qhw)`s%n z()SjpB%R7j?4uoo4};NR_{Xyz>ovfa`Os5pOqXf!$dAy^QpX=d7WR#L=#kx<;VqSa_n zRU3A$gc3Wp)?geQQg6Wd>0nNwnXcgX8ff$zi<_dsdj<1C9%6S}zhqEkwzK0PsRt-% zz1bG`^To1CI}D&8C=U`l=hkW=$FL0TUd#UeX_$09>@)+1U#(gZCI7orQpS{OruD}9 z+I%F6s}?I(h%CVwcDa59jy*u|Nf5qEEj*2AaG;`)xKN@(G!@bG!c-s>Xq1~FuMmt0 zhDri7IK(<+u-t`Ic2XnpB2%$}p)(vMJtPcK%;;30d2RrW0? z5JAcTdianci&!&PA^0nC6p(HFe5)N3IjFWYvstUGxpz4HtXimDLH@Gsp&EVgl?vVB4(4A zMzg}~r(qA5#9{R9X^%2nQBU4aLcr=Hwpv!mOoI}@O1U@Kvb~;M%EJnKLAGp=5sNg& z410t_&NrBcMK#veVdM>{pMo;MJ~yg7m}mWFK|uC<+IP|>rw~lvb&2wE5L0&0>nZH*$p5I5Nml6rhJ& z#bl*(hg}(YF(lT^wv!w7k;)w|G4-oBkU(&!8#|jX$$~m$u_j-jnHbwvFoLG8*3S+L zdWFMalEi47u+~u+6=a3QMq5WX3rVtesE&&ec!D>7pV5-Dd0sD<$6gPl5kyX%wl|gm z8qp$8Veh%+0MX1y{c^e9K2RD$u%)a4e=$GrRGEFNZ5r>q#p;8MDc1^riOaG{wZ<9C zU$A=YSL%r@C3zALCfP6yB{?2ZWmbEK zpt=8CzF@tb_t(x1{O^?cqssX5AB)*9J6f>ZSJ|dE$tWAdCXeELG8m8Y$>?)8^=eJg zoLAZ++vl!_KMtK&W9QZ6)LD$TvotI3&|_%ADw(Pjif4OGZU&PipO_H6W;mLp!z=Wf zzig-lj-BQ354MkFxfu^e!*LXw$smo#Y5qO*k=)xy4s`~04XVQ=O~)n)(KpgOuE&N1 zedAB_cHh`vJNJ$MWo>9?Z_D+UKiXvFv6uXnR@s03^Xp4td&RH2m-*M?>%*_bmsWAU zA5D_sATu=CB(%Q7shNEH{U|+o z-WVZ_J9BT0aIvdvZ;asC8~4Ttsz7#K@gscbcS?J^x9wGl6nhX{2azKAL5Or?ncLU_ z?Bu++yMHa{=r{XXtRD-ZcN=7>QAv!OZ~}sBcz$ooKp9j`sms$^xL<|vZO0_)XEN2> z=URU`F7`JnODPoI88wZ3E2`kB$GaF9I?k@<_y<G}kAO?y@e1_L z&l0LT$&*NcU3E-r*JhwLWf%;*X0#YNwwtwC1f6PQGN>jF^gGRF7PI_LnXcCpN6|n` z?hcB@L#1iCZ{5)?G$!64T&*Ak>lAh6+SgP9l;qYKyG72hb;PUZ4si~E&i}v5&SDS zOIIOq@maEP3Z5I}Xx^rCb+tUQ%Xctal2EcjwX{?BP)D8H6}lNXF-hL`hMb@9{#~nzfu$DgDe>*Q4*Vh$wtrfGA zZdXAu0;`%y$JSY~V;NpqI+z%fnluPIaG2FiWdhk;|Ba3TvnkzDBL_CGvK=8wlkp&% z#G@$5(?L3nlbdyfG*gqyCw_8ytr&Bhk2akA=*6~4YS6~6NcL7Hz0!#GpH#h zo(BXtR$xdQ1t`d&4`9*{#S`kVpJ={C_^n865rCIUV+G{sNUX!e6L<0`Qk>^e%rv*Q6aj6f~W$x9|7M z*L&_&S_XUD^}PJ&$C`rr50eUCGO5`h?$Ri6Jk$BVDV+LbcBv{$Jk;+JHwL|`?ICKx zD7C%B{b3=`SB?24?8GTU56XQ5L0?UUAuQr|sJ6{TTxVeiV$?ViaTSJ5HN|eoxcw&T z9V)lHMXfp^Us8oHFkq^Snk-|U#+Vl{;4<3nBOO#+SY*Ljs~TpzwO{}J1u8wyU+7#{ z;kRmT+8p=7=kENWSPs91XNv_IGSQEuv`@H1Nv?nMZc`mI8Hf!}-fL*?jg+w@1`Xg9 zuxz*-VBzvW8HdA7oc&=WaSM8DivBzZKk!)DCzx*%Tpt1E!K1Wo1mA)qN*unX!>|~< zx;>b9h%;SYwmQ{@19Q?p4>7*bsg^p5RX)I9(%IMi#j7)L_$8~d_EKtjL1Fo+jc4+) zhIUQd(s0@jJVZ=15Oo98d=Fd(L7!u(DYaOr05=Ijsj+HHMJ)nc=)$FTrjfXGsGM&# z_RA=j4iP{CEOHbsq==UsW+#fd!wa>^nYsirrH-K)jlGsz1M&$u! z8Y>i4FvT3m@E5VpQ;Hv99<8wLs@8Z6AXCsDa7}gk7T50|l*W$F7 ztCeJ*S1xI#JDhZoi(U3q8XBl4ZC&<|Ki*?Tc#kvckPE^wOA6ES_qN*#Cv4236|=jw zgYEvb@dXtplCZ)v3Qaz_c`-_Kzn{O%2+kxM`%)imK(@F9f6}0*o_!TxY21-hV|9*8 zra}J8N+S9(aEBeT5UMSpr=e9UD=mHr3boz6&Vs8tK}yV^U+y6;dmTbI9D#OX7J-wI zVq%jjPe6L6xVQ!8;9II;6^rNhDGj#SbySw{i2Y_Uf2FX-gPkjj>qrU##?e)M9$pm( zQ-?*|yd60VHcv(qtx(JlPIdsk->AAmJ&%%$_1?yE;nQI#SsR5y8R#F4c|u<-Y9nc& zHX=5;+|~Zj>eAx}q$nj77#9j(v?jkhZ`os@$6unhcKeI9K7n~+?QG*O*PQjk=i4fW zZ2xOCiESUw0e@anr1qDFQE2@lZ#`^GJV-~Gi4rp!46RA>ci=0V4gLJ_dgUFc#J1nr zNt0qvi?25PwBfBnCxdj7#N!~o)mf6oS8$pA^X%)F|Er*#*V(lCFaQ02XzMw9`SSFy zf3B%Le|`T_E5)k|jN^2WXX7kN^5GzjvtbJKr0sh5xs7G14SZduR!2JFfzcoz4fCN1 zLO{>|Elhp;EQS>lLfs9m2YPSWaH1x+(D+9= z_!x`&b7*WR#UaET{cXxu6LK^!4|!{Y->X5-DP`s*+!q}-DP`sS;s`tyUX_Ovc0=(Bf8ML%Q|Cr zy}N9qqx9~wDlBmCF3T|3dUshi64krQ`npcBqrE^znOK z=pqv{$@43i4*w?w@D>YdwZ=cI-DFSUJol2ZNd{>;9#JQm3`{=CZpJJgE2fCOv2>Ps z+xYa&hgN=AE~XC!eft~VyfFIIhc6%Qpac}*2i|KkgN;^u3yrtQYt@@+j^&5CVdS91H;&0d4V zei!|8REmLEUq}1)n>ez^2TKQQ{2__{aG;oOelFItpAR)3(KuTb1i+pa(bf(U8YbJQ z*les0=39B|^6~El^{&nCeqAKdW-+rc_vNF_&qfckx6N|Bd7iJL&35bhujV z?rXQl(XndJkDtF%9lfr$y9!4)Dz;zeTN;+rh%kF=8nv6(co;{I^Y!e}){iaour9vR zAUVCzQyG4rQjv%KlTU^^)cDWEDzXPyYp=81QHLm-?|!}877LmMPVZH`-tRWg(dLNbpZa^5Tn{@Q_lB4r$;ZpD7B?7_}=7TYfcbqHD6fjPVHg@ckCVS3_E&$PcU4-V2et?yKrL8=@4RVW9*oFV* zHJx&#W5nqUKGDXTn=|*Fk^@~!LDuTnU#`CLanEb){QX6no96IQ^r{)%M|w8OthImi z+hAdSt+@NK#?V-7#@uaCsu8x3I88NHbGP9GeFYPF#DZI4l3X)k%Y#`e%bn8gY}M??sZf$GjX7i_+S_Gt)S*y!7aP6w|?@H>^tG`^mgZRr; zR$+f3$3B30VrxOV!Vazu2=Twn9-I5I_%?s~05x5flWs9T-m3F3f4)RiHd+*te)8)_|bN@N>?`g$vTt^p_NtD4d)oF6W+G4BZb;eL>9kefP$aDG+h zTz;M?G#=LN>H9p`?=JQC7C3DO@z6e;%?MjSdzUHqMklG4kF(Ea0tc`;5Nt;W`bKNb zq7Wl++$smo+txuFdeFvC?ywB_ zmbFQ}Y&XXYwlU{-#6%o)S+AaJSd&f*SEQoF!fuqx|FKxUgP~5|Sr)V42J9*kr#Q@= z_q@i}&3VK6`Dr{xD;j`FSO;Llr5AHuw(;WSe7Xs1M0<#|EG-HenTXxz^7L}UHkG9_ z=(~&>`yMPWf$^tQa}^s8D#8_jGdXDUiMw{fu@lH#R$;fgriP_MI^}KwH(lJM5sm4{ zZKC&2{q^i}7|cx`7v962rH@_6c09>;0(YWj&~b4{3)^yqEaBUd#7isrmWHa~s0BeRQO~4Q(W|qF%P~>*Iq(w=zvhT;t{qkK_9F!!sOeuPq zUu4@mxuY&*QL6~?)^oNYoK=010Ld(+aHs`c%eTw-XUgh{wj?dxEp9S9(RRNCr&tL?@+l=p@s^Z)?H9J8!va$kkt}zRC2LYn%@K1wocZ6=%y=Zs@BcT)mXAltc#upc zNt6!9gUp!m%`_D0xEd&V`*Pus>HY#j7N#BThFf&=AGwJ7+sSd^=3 zxr#;YS1#V(TO4l3OVY>pEdHLw|Cw0)5*&md1|bh+VmSqEyjq+8?B2>~!=_;7QxVjJ zVo_j(1I$~4CoVYh0|k?T^oSChA%~#ousQNb3KlrWa`#9|0Xm@I;ax11rik2Y4#SGP z2pl=r>Qab9gb6BOvKoUD zXj8a2aYY5EMNv{nmbJoNVHZ&Z$K2q~@QX79&4=O3C?rskMMePf3Rn-o8UTS#FwR9z z%>WTv5J3dOQGp-=fJ%iD1rfNC3Z)IA?yrmJ!?2evQWrlJNs*c|7(^}Db>}TsOF#IF zRlD+K*oOKc%3rRzJcLI>P=1!B4A*i@*P#sqqSuTqQ&UjY#E5h!Te>ZK%NBP?0kY+r zDt&C>x+*AJRIa<|kcwrW3@T1H6nNQ3yM&|p8>AKIk~i#la$5_Ozd<2z>y&})=m8EH z$c_%_&?x15yhA;dy}_*z%Xd%kCw$uI>`joOs8c|jeY{g`o*(QD*;>9h=s(}FA(d;Vbhu>tmRdDnV*Va4i;l_5VdTNy;ZZ;D=t^?+eTS+y`)t@)gpt$d zM{-LHnlJ9s`R8|x;AH2J>+V?9(E;uDE#cn^+yS|yzWMf+8>65WG7ftJ{#C#R=#YpP zN>)iFs%!n#U#xmgVNR2^ofLDLZ0rO5CF^VZ2UB-QvIx{ScE!A9QP9#|%^RutEj;ZfY0by@M8vo|X&JuBSRGu1>>;{D0Z=momQ(?!&!<8!RD$fScb3mfUF5AU5 zW+P_fbzBVnMJhA6zfgO)a4CVttj)jPVe1fn3wHo6GTXHy&gxb|*Lj?$z-A4W@r9TY zO$F=DL^+0TmP=_%1*Vnak=B#wA*-r$;fF#>B zlGrZcc0RbMkQndGu*;U4(^$Sx=31hcEtIZqS2_Q)W&aMp#yrk{d0ZBfRq^0Nf8;9O zhM`{}M7wDDCb%HTh|^_jw-C6joNP0~@6#_1rQnBmPF?}sV||7uOZ_SgCL-9C3c{Bh{G z8XvA&wtwzr`!}lTK>MejS4Q2b@>$luNe6kB=A#g#|4EXijj13EzGCVaGU@iZK&g6G^uqwIS3|?bU9rZ9RVlV!sIyIX(kl?f&>#2 z9HRk*hw${i9LafbmB?`herstuuj|?forrI02305;KDbB!$h{BFI=JlTLXnpb>bUCn5ee^tqQ~n z1m1;Vp9-K3*LT51L4gx>0}Z)%0=M88aH|ViEDL-b*Q4-(gSUdF+2udNgaiC1tD_vq z1#XK?J{8xb*b&TXtlck{FSNOTE$Hw!`&z6Y&uP;PXa+wpGjZ=4l7m zLI63vmQGzBS`-e**Mr>PQ;PXuq1qko!gXgLm7SIVWfDqi%dPN|2*GxFAhJ9wGZi+7 ziISn=K&Db*;(GBH~{EMR&Y+8~Z&X!EtCE)lljM;DLlhP*$4TFY zOyB90`YNLKw{>^qUNe%{0s|$0u#XEZm<0{Yw^TL}^~q~U*Ua<=2PiINBP438?1@Y#x)~x?c*VAWR)jpVr~=l~AO+F8PxL1jjLo z@i8Pr6BZm=SJvlQ*}9CEO56uZlR0Is+4DCFeu2TCa4OD8no2{7)M4fTXQBkC=9q+4 zN!KtW6f)`M%>25W?jstq8U+)f9wA^%XE6;Yh6Va9Tr)8XN~g7@`FV{&dl6D&=!Pl_@kYyj>O#hHJ*k$iN}L+mPM(_1}Xi&-vA4D_+#w6nw+?~ zBQH0%UUeWfH;)I|*cg+A;O0_OjB9g9AgA@eZ3kceL_4TsXF30ybzDzI>A;vt8l^FH zlyN@3RX-V}ct^1>LbX5kHuL$T4^2$a?CC>6AOE&tn>aRuNj%9WNr)!S#zT`fJ=c%s z)AVnLnYr9BeYvoT0c940szzs<-nwb?wOGIQ?&igFP??bNOuf2IYh;t4waYp_lwugGLe3kUhY`cZP!5 zmu6Tt?0v3G5C20LA<9kT;d@bwGR78ngSq%o<3eWf)r7Mjy{IU@dd2=HE_{>Z8-$tx zu7EcCcppTLZ|gSFj4clnqMbCKju9c7keU=SzLUCWKpg|R6^-qB9aoZ%y$5Y#PhNy? zVF#3l|8NIzoNq8O?$ZfAWuNVUk#w{(z@c$*fW|ofmtQtpD0zNnY$kEp(oPT+dq!qiNzU zI>xJmgAL5EqCV+iCp&JE#WI_N`p%zrat#N6@p`LYY%|duRsNEVozfNZZ%0tF{~g)@ z!|NpL9u5&WrLqH>G=?o(Awr=FnG6vNL2K={=R+14q1Rwto1h68<_sMWoeU+`FdJ)8TU)6J1FxNX22tr~v9T22UN%6_0VlHtS%2ONQhqQxkA_)=UWDSX+*A!v` zE1G#M=T@Qnpu;}ZnGQL`n@ggaO&fPY#i- z=w7l&leJ^ zZLzOM5c@v~CbXiE(`>7d#6hLG+5ux8t5w|@rx*co4ANKx;W;D((H6~6bH1v_qC0P~ zYDUCg>Qtq7dUMPD((nI!dtLrWceI+v>&=*LW@rFrTbpz;4s1M$@@vG2m)qD>>KR3e z3~&lYFY%b9E)oXu;S@HG2ubCB@;IIH?P=m?YGOnyB;)Vi9>VBI6f`t{C7U2XHcM)MPIIqr}|r` z2*f%QMlr{r5_2MNn?&&5_si*fIpYgwUpjK1q6J*-&FhnTEDWHr^YejpE|o%TB9J?z z*H(6JmUN@jL?Vs+HJT-rEQz&oxon+dkYvHuuG>9rTh+FvIc?jvr)}G|ZQHhO+jdXe zx6irXk1yihKUuM}GBbAUh|0>n*84sypY%D8G6t4J7SKzAM0f4{XKg zm|j;i{Bvl1BDBY{>p5^|vdP}YMOAqiiL*kJjOnpc6-RNkb#f`8aX@S9Rcj*1JAJ`B zPEW(e5t}x~4*ulnfol2+IsMy*8zW<@xEe;jXL#O z+N4u$$|;(WY-#~asEMubUF?c%h0YYAuFbQ)-9%*_q4ymtRju%Hqpgqh zmR+LmIS#rC8kHPINNpCHx@>KmIoBttVP@;7`A6`zDh$55C8dfFv&!d9Y1^}WyhT&Xbjdio0!ga&YK5xD>=ZK`Z_h9 zpHnIp;qHy2q@7+Ej?CL!eVR>{r@`8&9eg%O?r%dFC`q}TO2>M_?=i)u!Nuo6pv>IH zwX?!}7f9Gfoe8mM*iB;k;cCH%owHlx1i=Tn%j~pEs0jRVb>_ce_$}%fug^SW(H0;pzy*x zE!{`>o}#vDn#C=I=>UF+*Y_HL_*&7vLctEnAX^ABa1;Kuha1g0CQR@rc7HJl(Ge=Y z>*?*wAj)iB_m=rWTmQ2RftarHcTShQQu1;r!ZPwhNwaik0&Ilvezg?tEJkGzq_~PWb0&{?OvZGMZbc)o$S`{u@w& zK-}Vub)`=dR;W-5Ju(&o`+U0AtZxODAWuC^llXx(a@y^MDl{!XjGsG=r%J?_ z^NrS0M~(LhIn=X56V=+ACLEo#CbtFGDuUpmv4tsf59s%99cHJv4~H5tJePrH(GmMb zkCHW*m(S(lDD8%qxD_-;_H6(mi7lCHL$7e!@9%1DP7z2!MAZ>Bnj=JGUG)ZwV?n1b z@^=q_ewCag6|7c8Fh@17hl~|sH0K4yCZTxDM*BB_EibYu3+7PM$NYh4r70Jsl{fB= zN2nBJ%4nQ9Uxh&>nSoc}<+6?31Q*?-Myqi|e~eGzLWueLh-dwRyB>$#+j-fUo6X%- zMA3^Szj}OiA{a%%_qqv@PrcYswcGoYCgaaskxX_OcCl6)xM6hGU!Uu}?dT8%gAGG?g<7BsWpHBRtN@mkz+Dj!_9uwMU;LZ( zaO3iSGsgR-SopyN)=7*|0*#`&Y7)>UWmxDK2@8m-GxR*If5_(Azx(+5$+XOD9S1Zd zQ{7_)hlD3?{#YM<>c^~CFb;9JE<}STM8IURUPzsWA2>lmK`qI{f3cO#2GNJXX-f=$ zF}@Q5-KiA?nTMEB8{|L~ERU}i0B(2bk_m+zO~4e?o@uPHZIPDC?eJZqBe9Ce>NaY;I*-uSGoduLY$?vawMYbI zz40L+_xNy5ydqd20J)`0WR{5hE+&*QTUG-P02W;6<5Q+p*A}BIzs;3q_b&?eD+Wm< zCV_<|f$CEpDi!+U1&)N{>!OCJgD;>!RVu3>Q6}I3bQEf!vu)4>=3A<^*<-_6)twR; zz+>U69oy)axnCuu4=SaDxC|dp#y?$|T1K#siv(Ff=?8Wffk4^E?sXalkrd~LnMw;6 zX{!JSIK|Jio*>n<{UUVq1$$873(g7Gu2Nz1m2C#}Nyt!u{#8K~hsuPi(Hqn2hrzBc zQc;80#V~NBtNINrvka-V0*u@xkRe7xL!1of_c~F^?7y0?L?`YyOGdbdujT-IAV|E( z80mGHJCaeG$RYMxQp{;A5H7mcsS6%~kpi?Z$WBNIB*X%tN&?E#eCRF@EmDxGT{(*i z1~o=;fu3(_sO7FzYZUIlo`j5v=z@#lqew+yV59^dg(b@|+I9ks%~coxd=VpHz>vyi z$cC`$RxMeR&j=chy=Ooj8jDT!8FV_>SoA6-w+9>78b&ju!aKR6eZEX56Sef_-=oHj zImlIwT-xTJr*L(7?gMl2C`obj_+iI*zlI0|#RmNO?9!N$Jx`Iw`lmOEH#4lahqxDZ z3gUS}UH)0>o7!ss`*Eg*eh5PpX27?VZbso5CDBLP)7>AvbBhyb@y+7rl#ZU%ShPalePS_ ztngxmeya4(%0{8QwXB{EubwdE{S}RM;`YMngca?Q%|Q%0V)IYx;XH*_I-Bg__mhsy z)g!Os`60p3!e#2Vlc(_A6Mhy@*Tcz)ffMUqadxMt=8P$h>@5_u>)J-CK}`0Rk`)}A z^BWPg&8n5`0YaDbyL)@sxZ$To9oebogV>+Xq3i7Qz^_NHj14Uv&w4>#Wi4hO?JB6u zt}W%!bNRYYj-7ch=BU!W6=rcOodbPb?&*~VQ4iLJzwlWxB7L9kDxE^IM+QtqL`6yi z$0!?(l1J>}2cDjo=O;TEZHq*v;cpa8p*@}4EGi-AXx5CmX;e5QXY66~?D&xjaa+MV z#*Cwn8c1O-jHTXeG-!;tu+qP=q68{|3N|CjE}?64j=QqdIMz}G&zNE3&fb{?Ig_!? zTfObGzYJyI?9|*@9kW}$SCx!s&M(u2N<`FnxGq)xB0&>j^gJanG5kr{IFme5!dy&O zRMJZ%hV^c46y*1#y#e$#9N{s`HG2x~1<_UKj8+DJ;M(BCaX#+5}mLF19>km94C?9FKICX6M419Px4uvsn5O2kk-M77}q zXv4<^{~QIp{P>6&dnUz3L<^P{l_<|m!P{bSSF?t~8B$v@|6?DHf59Gf{2C1BkS5HH zzB@-;0yS2X&v^*d`(up^UE4@-IwQwyqU1C7wBC2feU3M%TU-A_dvE)~CyH>u#9be6 zV@zAA*p4(%? z2&YX~tK8d1vcFmv9#;6ek69^{v2 z(&>36>2?IT3Zl+$>_$r{uiAthu0eIAbAucTvVQ8-7j}8sF%zX(Q(z8miu(e9lI}tEAo+?RNMS2L8(K^Q${;TQ z*(O#Y*}pNmOlNmeNh(vJ+5srsKM(qEYkr>CnQXEMCxOm2U6X7zBqM$EVVq>$4O_$> zib+ZB6OZ+QPry?36|Wb|eWs`B8M{F*#5EjI3oB3+EuSRB>@6q*lz$f!vkXir3ba8s z;k`ke#|y=!p6z#ZJIp|sGUY36uYjO&b6^J;q`=me=$kcl@PWCJgsKt%@-5^!JeuI^GU+E4jDv8g#3Ca3h`OXNAQ=DzWaTafW; zS%SmiS26OgB+gZH%eRv;Mk9&+2>(Q_@0T1I+2eX zdHpRfT=%V-jk$sqj|cY(#fMX;6}hvy3xAEDW(U_jYoqb;^bx;nyI@5dv`n-n1w}4K z$EJ|l{lQ)Afy1}ydhVhs{ATWg+r3M)#;4)b2?e(ce{i@;wC4Hr(e~5{y?XIy=6m*k zSMYdvI}xl|39yioC@m+kcR|AJp5D-}m2pY~g5dHlObwPz%?qt{pU@(Ag9zPO$&7Dr zx>*aflYGyny_)Sdnc!@^;hbMlEcF!p>A_KY&;CA_E%%z~W-jph@sG`SpZHJ&K5_3v zXskc~Y@kovKO5-!vnLDqV)seKGtrgD7@JnE*9^~giPubvljhGF1S0RE!QKht421FK&VTugWF;hfW+xPLayCofS>-BPceIh~(j z-fNa-JNHb6ivPLE#cq?EQj47i;eemv+9-I2-dFF@F(MUZWj~{#Q+_%@zl_(PKQK6& zZeFoFtHoaBsZ9V;h6dc@zY3C@*LI6HrsB0u)eRStI=AUu}iv@bFjY zK@eA1_C@oXT1iZoEngvE{h2$dU7P%;kth6VN)+Bza3rZArVkNd_DfL*b zS^ALaI%W@F-zSp)p-#%;m;3^^;v8kIvZv`mi_+*l_++}0%;4JoY+{OewwY+Tx)$5z z{wNsV8#j(@5r!|9WF@4Cokh66U&r(*8;vk}O1I-XaX*|wW&JR7S&MU@~2>cj<^ zzZvjMq?xSBrUmRQuLkHVO)?-QS8b@_<*c*>?f8Bxe$E`ZzkjKCzQzn)rr<4Ayi;ww z|EA_dG$E59KhwiKNp}3+$mz7VfwWfH&cjfIq#O9ep*HDt)MYA^m{gbmz%zKIu~!KX z3qUDbR+fL|r=r2tg)ckF)Dn#?xax@{a7io}>5-{SDs4JEA#^-;vi!SHZshr#y=~ZX zZ=>2?zCT-tv~nvCxFf+y@+ZS+@ytAY;(usqxoTk9)L5WT6rf_Qw<2f=D;TI$K!0mX z)G*WHkQ`e4t|fXob6$4s)lnp^(rxT#2u!C1Kad>tc0?A>x1T`=yKgRZA*$7-%uw8J zT7xOraL-eX(Wv2Wh-%hi`-&CvVF%)1qy_v}3ugO&_shU^Qe`@L#PXY3kYkXdSU4SXmp@ct*S}9d)I?8B26|JhzOEaaO%w z&L&DFbd8lft-E^TL$SZIi{t-ats@-&f3=RNnp*!Kts@Z`RdeEUF)(jb>D4kbf#rV> zhYGl#|B*U6oB~QP+^L+9OOy`vzYj$WMbSzBKGr373>oiuK3{A#tIdAcyVv;pf@Pe9 zMsp$1|1X6jg?U$cz*5+lHu?;XTIIb` zSTt_de6az24EYcuJ6Nzv zR+GTq$05HL83BRexQP1@m^ip)CLsKtTD{VsyO?kgAxwxblewl?0lrI8hRMGGgllppR6r1atSIJZOTsu}m zM$R(sBSbYE`b(-utkSjx$|ca1ZG0FFL@#5z#52R!dnY@|XF=cG$6(;AZ2v_4v3wpb z~`wFEpcQkcR~BQOt*+%nTXYG$GTZHs7g&KMeL1`r?)eKnasE2bNtu&Rjz zue3xxmb>NJk&H)+#Z+HxeRk2rSKnR@S_)BzfR8oY1uz?UYsL6EvL-I7XDTuLfAz@2TY(io4*s3K-T5hC(?MHtx z5%f`6CyS|42m8VE$A(k3n$Js6ZV^xJIEkAA6<}8mj-<)`XD<@uO z2eWfrN(&eTNR51det}L979ej`sLLf-7EIkd<*00m0HYFO5X$Grhb1O)v5kUF$^Uqn zZ(mY29JS5k&?8^pVOv7!^`(z$hquxqjWV;pr)MxsCNWabXOy@m`*w9dIKr;4Hi^m> zz&=HO;On@zD|Gj*QGFSy`VP7AgA({LyJg9-iP!VaAf@1FRpX*Ns0jsFs;~iz(*!Dd(79KCq?+V;fiNKq@ zsj9d zi!v!U-in~v=~?Sk_56a}&sfZeB`DrEq50i@TPshBX9YnE8Gk$MS z5%p$&jk#?`$Ik)q(-wuylHvbZ{>I-5f8vm^6DjaQ0)d6?iX2`LC18(2m>7D<%Z>3l z&~76S*mI4S^>4*C8DkjB*lvcW*KEPF?8;RZ;#x~t{%+J#>vp0<5>Q_JYq9b5xqsE= zwQx-B*P+$jrgYzhWNj1p{u*U_e|dT9EwS(}os@}gSBLj#zl@{1x#vB)vun$AXk)7u z@Q;l-(%`jU;^uctnD$7DVWx3i4Bf8og=5OmPCBF9k&WXkgv$r&XsBcTI46f<1pZWt zONWzlBp`*;SX5iQ>Kv>%1<#4*n6>SaLRgp32)<*~q5Z+3jsM1a!SA`bMfiy2=o%MHcbCS@)WK`!%0q(d zT9_+y+g|dOq4FDZR*Z@+Lt!hoRA;FaX+?$Z)`if_o}z2?=@ch7B9hW5TT%L;kWAg5 z0_y{N?dH%Vjx52O#u!)Y2GWp|rI^`7H2$mR7J^Ny>r%)3oe3G{INAj(dkB$I9re>^ zjEF85X2FLae4q^{^-|`Mi zw}~oUVZyx2C)EL>*s~Z=lHeABX23S6nd23U>|MpI497ozYi#=1Te$~;{iopn!26UF zfS&=A4nVeh@_OBbk}#N@FzNVtPv{0d*l>Zsj-Gu-Q+U6x3hWrJOfiST(|`FpuGq4AS}$Id_*@c3&ujLHW?qwp_cA*yBvBl`Oy{h~>)s%u%$P!1KY%X%jo)#X3O zgj^RTJfw=009x1c_2#g5!Cn+VXdvlT-bLy_noCUz{*g-b3-Ff>BHW9keaZi3 zq8qJ&YKBv4Z3f=f_BG7t-xj%uCj8s7PiJGl02*YHzWJ_lc@p?1h02q`c7S4%U()KC z%-Klj!%}{wFFA{n^L0$RjCwfZ5@pEQjQvev53PV%Sw*dYP-Hoky3`*F;LIN~$wR0q zHb{x_SZj@A(V(qxE)1s82Vy-P%EWrAR`^t>>RLkCS-?`PVhlnJc)+Ly$_DIADkcQzjKAdDH#1qm#soplf?e^_`{UDXLtZ+uJ znU4xR~fRV?KBx_&4D5iH>DxdO$?{IF^d1%8MvZMHpN=5zq0;d9OR%KmL6IDzl#g-F;2P#~!?*#$zJGk2yE%(JYY0I)*J)Xu180^W z&A}R?J+a5)$xQdw#Hh08RxOoVciocLpNtvB?e)&{4)av7_Zs>jTyz18PXg>cx$ zY*^;)HXYkC@NhybW-VXd)~~y`BuYFzlz7uL!dW+k|ISShDwCW&k{h9QAy^F>@*8g0 zr1-dS8%-t^zS?(rYRsP9cnZzCp`V?y#JN;4^b*C#=v`CR3C1y5#) z3i2|ZK^tN#XZ&l{yV|Kvndcc(hC`uEJ4RM1hF89210gcx4Ws#E`^V3yM=;Xm_-vy! zgZ}YMit8ltyr|WunWElu!i;|$Fz0HTPcsc^_FHDSML|?dFwMV2M)B5>z7WwP2vq~k zi8YK)g;`b}J}CIRa<#4=`plI`Ca@IdqW)g93y}^ zYDAuc5fmjRge_rK%&>~mHQQCQ`|B*kIOl44Liw&aTQyhE8`49Vs(?BQzLFKC*#Kkh? zIs^A7vX)Z8Q)wIcBX(}=Ly>_A<9q6sk`A8p8W!N7 z=K&A7kl^W{ODBR)1Ojr6XYNxhuYGRl9+Ija<8KLvJo=xV4}B+uHczBJKwjmPg*Kz- z(CbLxlKbM0?s{>-vUO3wZdssINwEA60~6IHxbVVBa;np(Hs{4Fy zF2J|~%^jSdWGxQn@Z!!f4%w)=RdSCrRiwu<2j4hCUgA&ake(MI{^LFm1>-F27i192 zfkQ?&d-de$atx82GBdUYgMxl(gj%*ZA2uK$OY$^zAggqctQNH!21NZ$bC>LM{P5h6 ze(xMq{Gi4JDo9ejihR2#C1L%%@7e8m4ZDpT4v2~Euy{8^4IyfIDvNa#^gcY#W`a~X z1FkxBX1Pf^{edye{vPQ^DugWlNEG_&!d1+%awdgD*obn2A_J5Vk88F^Xi`YX$a+W( zuSXmbKrVPYmi!u7u^@i_zI15DbD^oNt^SvV? z{_6+F$!j3VRfIYJDoZR~il@6J#6T5DstEG)p9uX&QEgKx*A83+WtS(_G>Qp$h%z&> zNV>6o)blMsFy*0o7#yU^Fpy_bn?XyWLw8s|v^NTUVJOT8yI?nLzX%P-FLpSLZGm{8 zTc~wUfumJK(K*DOb5RbR%R8TJwA}a3#f&aru5VC`06nM8^$kc_Rt*gnsNbtCWg;Ps35zHGoseq* zMb{qT#zXPJix?y?m58|jV_gxKo{Jq$M2@W9Tvf;4dXLUUgY!c@m`dr9vyi-|U*`~J z5SGQszYS|XyG&xuosdbS4;s97iFk*ZxA6OUcp38(*5{MEZ!F1JmXTqQV}r;6ApxGL z$1VHd2!e4VAu|7356ta}&`Wd-?7EKO2Ms(@8xoL%;xTlsnB4~EBksolS%c!p{@R(T zi;Kb1xjOf3IC&KxvOl01H=g3|IMK6I9$Es;{Rf+#1fy*kh2>Rsi`+wg`Q?BsRAVl& zsnN)K#4aK{`9b2>)tyjZnjyw3^^pJ9f6}2rS13K#I>)*ZH3&WE zy3iZFVXPsh!78<*dJ1mqulvVDJXobU%dmyw0|>0 zKo-7KqH8&3;XH4Z)A;d9as&D;pp>`_h5a0!)L^xa-$t;cvSE+-ru5agprbeE_c^^R zD{_ z&mcBci(ygVjV9v-_+Zsb6#QYcVg%D<8q%WryA<0m9s@^M5rOIiGj{Xl8#i07P0^C1 zH`obZ&gFG}iItMM>!M4@bhpUa)6Bwysx;w%&W!jF z%1M3c!Vi=a7E^H}xj+cXlT=uckhvzdy}~OKW&Dp1Wf`!?(4kkl)FxYI>V3tW@=>Mn z^jKF3rQ-w9AliYdW6kEEqyBJkY3wBqzp14e^uT~u4n~|!5yjVr9@gpvpvChqhqDWg zu_}>ZPLn!C4Cs&{)1bxx4_4&?Fe!E-sG$z}poOY6D4;86-$hu1Yi*zYLn2f-Axj-? zcD+J$r)+cD2Q3An$GDkc)^BPXYdOLDo)NC|P-hJtiHcBuTXF8Hcvs0_CrBM58AoQq zJUaO3*oBYo%&ZkzDyyt)aAaUCutW}Y;~KMH9MhP$`O;mtl4t1Hw!y}58+MN`4v^=F zuD9gElnzdYdf&KVH?$csTf`fsn4AJg?#mg;NpbPWvkmBxV%V|euGCrDS z`0Q+Kj>zJ=MLH^L%^gEDKBe8A>BUI0n;M3$*+Z7jX;~zMOxftb*BCi9z4_aT0ood7 z+atc#b*V+tluOUH)fN87=Y!4=HQriI!4y*#`IpSM@XSO>%aJiUlAbSs06h_Ni(VIM zoWJVQW;9XbAUhuktln!a=kSJZ@3|Cu1-;J7XqD>8GPW!ri)? zoMFr1-oyIK0LJb=B(_g2dmu6>jI6ZZ|}|(HmQ?mkg$C0!Y&ywc5yLx=~4ax zSN5+53%Cip#OvRc?fTHRvy~Vm=Zx30mK&d1wT$b@XM#J~`-y&iLH}pz7Qt@${g~JR zG<<9NY8K>?)ALXDcl|~t$c%sl{&Kj`05Gu?>xAkBgng}m<8jQDH1ND0)M#Q zaZF?bj7-6&+=Tyl;^-4)hPpbxMJW_4AQ*`F{Mt#wi>#liL`h`|?4>!PkKqaJ*7^(Y z7|!(IcxV$7of(`@JOE}m*!9{FA9qFePyhLaLOr38dDkX&d!zWmShe`#m_eH@1h*uh z-sz_{fW^HznvnV^pff3zIabuv(+b*}Q_MAp%lKaHtIlXMJ!$J%a`>vPX^Z?YTk~Dp z-hwxgUi1C#Ho;Dj@HoEhh3}VH=ja~QR7ZnoI9O80+T%hzh)a#Lck!!@1h1{{#|-82 zD-7uc?UA){8a3MZy7r0#Ez41_J_Qz)c&lg%^m1cb0sCdPyyEr7hV0C6dP&YGQ#j<; zhIYj|_(QEviI;Z*PIHelS@5UX6PHKDJ+wuNn!Q?fVF1b4K1D_(L;`aIP4Px7v;!XR zhp!l0l1Bj5}y)IE_e?K{ij25 zUE&O>64nB#%7DI2_jVElf3U~3AMO^)-zrljWCrFQ^;z#|eCvjuAd>ecg4g(GYk!Y5 z^cK#HBTp)Uku1Em7f~Mi7PzWzi=ucEmaAC!Nyq6zKc-oB4GCZ~8q?tBRaA`uQ|w&D z70@gfumIu63NyhC0nx2Q02^U^O?QPjFKU&ds0t3FR)#8CO@q=u-`WU83yC*zv|(c) z@olNdaj48wxl&UCA7KRoMB%?36^Fa6@i$D+2$tC^6Gs)fk}Z2#W-pOs5OxYcgBrC~ z*TkkbKLDrcY?2OGiPb5UhvWCObo&_QQj-OXUsuGKQC(USUU$pv>4O56P@I~pDAdZ) ztNDtR5xUP)(1w44YEdYL8^ASc8`&t)Rlp*1$3OOj^{28piq$R;#*?GF( z`hg}qQJq*`C7{KlD0%Z!+EE*}{lE^C6_3pew?y{3(YAa?sIDn(U( zPw2EByr(5up!cChuX@wWmRm&AQ|MRz6-9lIB{%*hz3QLUcbFw-z+N2o(xJdJMi%F@ z)2x%Vy08fmZ6C53R3#(7n@4B>BM1}~aV*A2^0O(J)0QI2_^xg#oS?}>3nncw!=3@q zBgxm4HqXWf9xWDDbL*1WkUIj(r;I?Uq>~wfo!wu`iLt>Idf%mfh1=A;6?C%f}J?uEg=^ zS~=+&qjUFJYzjfGqYnMn}NxGxG8d7V)?|I4^Tets$89u<QVxmdN$COpZlh!s8_oVQg%g~Qle&unaYUCmuiRl3J7pRtAU1A2w4 zsY(6iKh+;c7bS7GA#&m7Gtsk1$Bi-*2M-s@9JFWZseP0m z0xW!U7fJ#|*1aFY0sws3C9k5zAw~JQ#a)pJeHBeq&UNhw0VCy2oag?(x&>ZQr=<(Q z{CAYQM-N?=`uwDv4APe$KM?Xpbq-o9w`UKSZU6rCizAdNcUBsrQlu7K#Ale~&Rf=^ zgHq2tbA?7l#!8|Dgg~`_w=3l0I$DH+`vMqh(O?Z{8kN~+YTtaE(&DRUgav?4GqC%7 z+$GAt!YH$XScE8%h38S^1!#Q}+&>XHcNaYmxlc_4HyrYKq*@QPSyZwl^FLn2sD4_` zv+FI#a`oX^o0{)Euo9$pHuV`{i}Khw^+K~JM+wA#9m-ftnuhnviE+AFi_(HKm z5{RA+bPI6254=k(%@#z;a3tkOrO+<0Lx z4@G&e3tR3JON=I;cTpX@WK*`M8ycc>KX9&z<~H1#%YVLnaIF66Y`OoKbk1h5ZGQf? zWHq_@K0E8!&ClhV{v`Y4{dA2+Bur>doOd#~6HN=A9#|4>_0`~Sdq4UY%&9UilM7&LMA6JjH&W;L5^A_;2NY44=H zWFBEpy}Tt~TcYB8q5{~(nUm2fK;yTOAWWE2`%jFjb#uI^S53XBmVfqUMq-6hE0GV3 zgqZ&jnx{VRHE=B12^5d|-UWNQJZ0%d+#iZiXW`(P`An1MffLVGS9shaXYvALcHUtx zqv2xx7x_+lR4teD+Zt?$zN}tig4*0)3Ggo1Rf-4v{w*Y=5X9fLZc_dgOO#*~jC19( zrzW__3FE)u4XN_P- zCg>~{VGLeax5t={nm@dKEm_?JKa^Xqj6t?(2jr()WeIFE9K2nx`YQX@La5H5>bO%V zzR#alnp*NTZ={S+9!XK*oMp;5qwEuo;W!X zyJ*+kktDjlI6g@%Nu%UTh2c1ng^X(DETwOjC1OGTQ-AsDoMc2$3SquH9iKi)HKn^w z$o9^ZeLMRoT1cMhp@ne{NruE>Zw_ZZvLyP`aWJ|3l;%>%rVr^!YE;VP(oV|tRoUJ0 zEr;)Vv$E}vTu3BMM>>MrPn~<_!CV7>)U{`1Vsm?4nzX(9*$#1|%&z$o)ehp z`)t%&Zj1pULSET_t*}N^QNkbN>7+x>8{V8Xej{5^;2ZkHP4g&OJf*Nj?N0=ehNi@s zlxNeu-#*n~*xcX~-2JBvSVq&9UpMW}GE;cCqE2u!)eo*Bq+o%$%T&p`@6737)SCIS zuj(#@lS+OWmlIlFf0_Qa7RgV7Lb`-_!@bLY~~+D<82$e(;xy~Q=+*e2#ah}&>pR_OVj zJbY@l>%!C!;o->Pa^&fFe`Dq^U9|1uXS#^Y&-*`-g^!T z?KHiFRWRmG25#MUw7z!8`)yE>xRh#oo8j~BuHe5P9J=G)Gw!ab;DzGl`@!$vy&a)T zC9pfMbyJUS7`{ns)qj-*1bRnXT#M0Aefqe4J`TjBGrZeLmrk36Bu%PM4lYy+z*!3d z7B%iA<5It9u>9HY|ES`BS0Ks-6_>QcGzse+&M_RBY-;m?-k@j_1Db(DS$P1hUZ8ER zT7U2oJ5@t_h>M|Hjd)M-{mS-}ocOEnss*$U+itwSBG_=)qxM(-38NhL>Gr+R7}x*(_{2XxFsJvSx2B=cPh%9b*%!iVK*g7r zKOY}Bwu67dHjmgCn(A9p9+BPfSBdqR_Ak`<_g`-(=+1EVSwE$Df>6Z-#9B)Cu;!3f z2jaA?xN11WW~;*|K41X}AZZ%{hSi4zKMCpn8s4d+t#9mT^sedO54OFh6vW`ui4GxB zN)~ehSB?kNqmV$gZs2?Clxe7=gre?WHDN8_WRA>%(!qOaC~)#)j2XW;4ExsXAFX7f zOSOUW&Dd1xaNlhqZT#PT!LS2sIA??}F8_(y-6J5FO_!W3qf2`gTf)UaY7=OhssSJG%ij<9vZ^}UT|AGN0_S(5|eV2epyYt90N6zdCA`{WPRpbOC|iXjC^hAgKa3V_Vp}hlg;39-fVJD`_?X~v+Hw)p zWGPq4*E*-oP#V6C$Fjjvu>1xnQvF%0<2+E^M1y?=hQQ%&79VN;$zkVAeIRib>`{4J zBjtmk=;0oOcErI$V-zT)#WGkx|J|6cmB5a^)dPuZmSzxV3L>(m*1%F2ZlWbahg4GN z3-sh0G8Ut3&7<8mqa@EZLW>@4BB|2)qg3eaE1A3BP93equrk#ALrV*{4F92}apxmZ z9matn!<9p0+~p28R|{aP)D~u85=e5B?4vRnih+`&XuJ)m^L_pbiIqXu@zp5ki?%SK zP#R;Xn||`2371>->?9pASa9~lTaq(3?5C4x#*?=DL{;6D1niwW5N!cyqLw?mG>Xwf z|6y1>OZlux@P)l^8NOsuDsu1Y_Az&^Q%si7UlpempRZ(H0U#&)tTqQ|~G|M1XgO$r6jC7WKKgk2qf$sog!)pvIgi4?y39gQ4 zPYpRAG-yB~MMZLUI!P(aZ^WrE_>=!cHYfGU_~icLJW~itBiF+3k*q=f>jO>l*DoP2 za>5^H8Xg_7VZdLw{NXj+)~qj=WI60ch0Q(*lVhprcsx4W1lH zNv>*vBNhYE;wIsmMLLCzhvy=f)P6%T?O<4P`&m{Vnq!p)vJt`Tp{1TJ z$6-WkY%;<GeH9=#euCY|NLFpQ2)Ck@T$ zmdXc6B^Lh)3PPw2Wt=C+9rPY>5H~s!(5wFjDiiD7+ZBa%F8PHK0j!9r1T`A3e+_8> z28hHF1?WH%V=|5+joI&w0&$^GM5j^zPL(@GlPKxu17&DJIJi2RMOn0gvw7vT9{u{N0}Q7ctIzZJGdl`oX91s*x!`mlLZ2d>1XF4e-8>#fVw zei0#aOlKTbVMj#gFHjwpi=X4F-&&O8Wp{lnYndwr@TIEYDvJVKeIAlm*I0eP-WI58Jz|D{xDUXl7D|&1xeEcR z{}1|+$aZjtWt=&~*H=Y)xQWIt4;UG1ZE_)kd?oh?cU9AcG z_)2j}``q>L$D#9T?7W(sn7zi!?5)+@f#7Q)k)_Eq#i_qqzzfgw%_oC2Pls6$o^P6u z)7lshMBx4}is^2q#bQ?PIIYWU=SlK79VA&cq0W;H@=BuZ4K0Oizclsyo7I7rNfNbR_a2B_iyvzcL5Ey^NBWg^5JTrtU1 zPkTjV5O4t&kUTFZdSe7~Kn?+VUzj5eOS zNYioD`y(mM-r+BKUeE6W7z#GGusT2M5aFg`w_TocC;rzswe}~?4FZ`lMY3bBBG>}4 zrNI(`vIGX06~Pk)m{qDcM~`HXTl$iD4*6QW3*XX!5N*awHCuXO^HO zggQQU((!LxYW2nB9BZ9{*|<5E941xQZUALIGl=781?cC0je8oS6azmoWIq&->^HpD zKuZzCFbpd_BL7mE>LY;t#nGZLN)tikapinQSFoyVL z%axFDIXdWY2}lZo+N&xdP_;@2eCh>tS>*DRpcE*g>VXSJ=(MH*32BYNtyY>0kgLYj zy|g+yxK1oULI(uvaG^!H(6v~49M*D11&gJrQb3vzQSpWp&|%?eOz;ko<-%l=K#5O< zL?)oF2x!j2f>XG}xGf+Q>;Q#lOs*1>4##w=1vbrq&R)5r;w_H6`N&6OGm7x=t|uEA8C>_0O*_#g|rrD%E(q z-qo1KX*wPa;@p@h8>NHEXl%FSpP8p_iN+%@(YW>P2cj|cwqiw#DMfP^^r<vW>H3 zkfswe3PCnblH8Jw>2Gh_XIh&H2wtx14=;bG|DqEB?Uh3C#VR`ZikHQIF7eRo-TbX_ znRY45;_bbDb#H0@Rz}0iVMaY;`L>?XUYPI+6g9G?=xv|MZP#?Z-g~NVqH>iZZH$T&P`sYLQ zMfNacYnF;ekCahf?Czs;4ZIc}QfVx~GIVEnDkZM4z&z5wUrVhjBzf7#oM# z8?`SV|4sque{6R5>mu?0DUAr_yP@g$dA^E@?brF1M$t5yzdgh zpFiF2r4(8{YHJfDTbiq)hjsClM&;=@=daYbudD5@LUE7mzQIDamjvyLD8^5ySsVF^ zmJbiRRr&iA{wRJe*3(V&ynK3F+6kaKJO10DS2iZ*!&RDFW{(7yDU(%)+3hfXo+%J& zo!+PPu7|dqQzrPAa=_F0UQmavs3p{X^RD%y!ROt-&M5S=+xTYP#-F)4HyMwHgV7|8 zOl$^uGM?OO#vQ3<+->nvMq#6GqZw!MPdWX1^-9vZLRDcur+O(X->q`N)Lxc;WSpH};dE*L^R>)`y-eTk zx;!Z_R~P|w(G2%ZP$4YXt1%T#niuIa4sCi$aoWrKmTg@=*KO_c|Igl=z)4n>XXE(A z4R=LlaT^B(O^WKfFD#N_*bEGypiw}_>h7BEvb(FAs&1Md5h7?L0xr=g`WN{Gkr)mB z5d=kXf`Dk;f*6;mAg&Np6p$U}dvA3ucRlxg&ONugM<&0Y(W$;wb?V%6&v}<;vD05( z`f&-|5Bx2_`iD($XkG)qvndH?lChmAl|&nje}YBl!nE;}0&4A^i_D zQR#o6A*TPq?EN8JUI<E4I-|1z4tC^=OTFO5|K@M{_MjXHf5mOg)6W zQVyY4_Yf8FGidnw@CaJhY%T<0UH2caq)=~hZhmgfI*PzrbQ`8y6OG22-ZesbAu0Xs-t-JtZI=>HQ!QMtwq4bA&9Dt6@gkdXk(94lcI?$<;`Rd*GrPpu~rq&OrJ42W1M^pi&u- zssgS$g2-mUiZXn)22{=mQ{uqX7eF}6BDnQ=35PI*9U-$46Y({a5ZrJya1xBqC-`taAFOr&bon%W zMN4UQJ)tCkc$qn6$fOi2_X}6#qpjQ@lAPg~iUKG8GV~E3I=YOA2HdDficuIKhYyGX z)eGpzP~kvf3fbO_STqhQS4SDjDy`-Px>^{I)=(;{7gYumZflZlD=9?=sMnRsloBS= zD1rr6WRmb8N)b<2#i#}Ou|suuP>e~j6q}TqFfLft-I-_R$CpYIycq?%nHgkOOuULI z8pGL{LA8)FyQ?g+orqV6kgPe&A&t}Ts8Ivsnm0{SooKAvUxzj5Abz)MiN-MZ-BH;p z2N>F@?jTc#7}DX8;nuQc))KqoT)WZzCnuL(C71&VQ40vBfH8z81CC-ikjq($NOqqh z=Vz!=RT!r>PBYjLS;99kbORD3mr!%WMF=FM1yTiCbw!kZNqL85z{CJ@qX`opU75Oi ziq@d3R8T4@xMe^ZV-5@}G9fi3r5L0^{w1drR>@bp9rS2TC0R?8WP!>u=-g*FX|B&@AfzSqM z^BwJqLtSebZw#hHBH-x)RiVL(B3|pm%UFm@3)2fj!sZ4E5n;cW0b!Vv=|IsB`;_7B z@u9V(ev79)m(I_*g?fz+=mMKOgI~M8Yh@AT)-Js^xNvBPN-%#OtQ#X)Z3YLzRbtB1)R3jaXzSB$1M#w-Z*s8H-8n z32X4CM1QE(2q%o@I*y`B7!*~t(hE^n$+10tKkeLK22RF zti;oo4T?$-C1rV1C2fYnKM_Hph-Ob*8z-TX6qnVB>f}V#fMSwDL#?BpU`|+9XQ*~0 zP%(x{ySQ&G#pcVv_%h3`q} z4d#pr0H|i6_J!ZTN1h-I<1_$+cw$h>Un=}q_!W{Z<}_5F@Ox6)cf#va2n)jN(;Cde zH^5ZfWGPoXu?bjW&IXVIqsqB+T-tPM9a{X^SpXpMj)ZZfz!i}SZ52PoDcE}P-Wa7m ziGOSkgHX`R6@G3;sTz0>=N+l6mz;N`;46ghNJV6bAA$@}p7^G5J8bDpvYa0shB*r_ zO?1n|i#b7u6#qtu+$urOO(_+NR~(n94~UJV!Ygy4tTgzn__2l53j~}+afFhJM-@LU zshmFX{sszvOrvKorU;PMG0WL*qmVP<#VH_{yg)R<>=Zwa==x`Wa1wZ%ay`=rJ#{gT zWL_WNyU`O!v_z8;8Djl1oNBuF$ij{?v&w_XLCPlbnN^#A2sT z6S|_d=GlTTpcxN>n8!W#FvAnRAv@qJ$G6u@>U%iq?1%uD7@UpCcHIRz4@`e`z73Lu ze-%V)=h|!o@O0Q&Nz!mjRx?8aV;a^|QboAOY{f;yFGndDQbGsJWo~|khmA)#5;){kf`W?p*|MD*F|T6S zBNWdx0drFssxtzKmw@P}Frz_6g&JU=gwHk3E|X%zvfYqD@)KseC&_leHMIasdr-g$ zc8dnoHb4rdlG=%`uy#U^)Ew*d#jKs9^h8H3br-7lNU-vfajm6R`z1w9$5BLtOl3Y9 zs`N9{yLv9B&1fp~7iOBYOQ>YvKCD8`Ba`2qbd(vl!=CpN{1}i9l%;>@s50`El@d$R zju?=7&cTnl^NK{>bhR=Im`sZdFC3H*Ox1`KpD~M*wHYcZvI1Z>Gcrws$Ao0pOo+Sa zsbx%K5^A6%M2h+cgOAABqTx;jq}hmx9;MJIP1Q6jT>dg44=NfHRUm0Ypn4l;C%_eR z6AEz3eBPs~&pO8Tca(GPWEJUBIM7~xO2!yax4;}A6&A(yWnS+L#U%7n(viaLCWfPV zBG$~lG^5(8zMB?}69i)q1=Z#B(1Pc?o!dn-kuI7rjSAVdvPvW=x>mwqHG!wMI0CAC z2!0$RpOZ4q24y=ApfmbQ2kaEaw=~8qs`m}UT4C|4_p#iJ$I@nEb7Lu1+)Ar&8p4U1 zq{ji$I_oV_#+BhpnFm$AggvyO=PhFrgDDRg>N4IjNY&{Xq(-F4nUWcfA3|@JqDl>Y zJf&KpXI!j;I9y!9yHR%|@yX5k zv1wX;Y?-_ijbnp9F$VL7MW1Dd9l!~f`qF1k*XRKq_A+E8KvK5u?5HHNp=DndIXtp2 z3q4l_0>%b3EQ}~*FBDOn$5MY896@Jsh>DT$dgvLfjiOtxhekH zdMW#5pfthpAHF{1`5#0bV0vSO=>M9T)>yMaEx7uc=%2+7+KypY9n-JamQmGh z&$9ob475ExQy?HZNvMyu8`zFpYJA^K(+xgogqOF>gw}&3^1L+1H|cY?B)c~(S}oZe z4miwLkyH5Ubcb73$2V`AaePbA8-zFOQSY=ifNb}KVY&cIo}`hqNt?3`nY2k%iDWUV zy(l(>{w>f9wYMoVIBr2Y&pr-vgR>MOW>%>i99rg(NO4to$k|E?cwnm-ueEKvc5~d? zCW2)z-vbUQxBrFu#p%lY;-YwcUtJk(#XR8bZNdXCofDUzp8Q2w#~DhkrjzU-$r#$EZiJL=(Py>)m|a*HUt{8dJNgdT7k$k1^20!fj66ww4hC0Bqr(SOJ@9h2HN7|) zgh=(WC@_GnhP_q|LJVAAWn;2d5zy){20K+c%4wN^OR2Ug04B&75yKR? zJ6&Fz8S5IO%Ir{Rhj;!8cEaox>V(O(w3IZcj1}%Yp{*Xw)BxK)*iH{A206BRiG!WwjV5CSu!6D4?GJtjYU|Jj<%f*-?sLqlZy$Fzu zi3Q_gx+K9-IMAtV&5e~r6UaaVKuR{)RtH+&ST7rw%78k!K=X82T90LS36V|>+)OD` z4nd-34i~6%s?E%@ut4QVMFbO~Dy*!#Rkq1Us$rF_#FPhhtU^4W2&2%B?_*Xh9s&AAO( zk#Tq&3T?AOD_Z?7KeQ*1-sBm&BkQ|fQc=y3rPqdfBZtig>1_Cahebu^+Vc@j5#2ZR zs--y<%QvdFr|aHwX^I5uB@&_{fl!E%DHO8wgL?uYJ*IfOF&;3K1qvaCRn>IYb{!Q8 zA&y~L{@OM4x8~d=#fDT}2rx0UlE>+vOS->$rKdX6y=voFyS6~{MbF1v^bY|gi$Hx{ z=3f`y7ya{+q7F@Rv(;?ZDd?k3kRIRQZ=Izj8T>#RL!~Q{6LegIpV6VOr)NA!ZDW5h zDhKu2%$<#tBya59fXZwzkpw9juT|y(9^zau(pskz5KWi#c3PX>Oa-(PsR!Av8I{g- zJt%dxHU;5pg{l!9f|pD-W-Ao3jZIMxezHE(7$2XmR}vG`eM)H8Cqz@NS!pykHKqfw z4}C>tv@ts^Kyx)J zbO%SNW;s=9jg8IEb}y+-Pu4aQ&Zbl8R=jBh^oGhNim0aNMKc!h3Yn;zTFjIQaGO;jt}6wFU>P4C_h1~k!)n8)dov87KQMD=v* zCu_5n08uh=|7dp)nshftDHt1TwWgAmeQLl8)>?M&+kwi-LKImN2O$O-Co97aQaX3| z9n>NPTXCa%! z0D=Rz!W|sm;oxvDM_EfT*${V>p(xTZHI6b+5QtyQYB4)7OwEz#tUHPTUukeYMwPO_{a>jC5`o&k+w117%kb+1A%iagY^W`W5%d!AW(>l%mJ`a5)FC~ z%TpLMU?>L-Ajl;I#vIf_k2g zVRO_$hIpVTW>q14+XV%M@G{N0EcDaGS1zp|@io15llPT_%~AdNlZasiRfM>vY&plz zSPy3-I(xLWJOx4z(#o(((D2VN5zUQ<5pfsH4jdE*l=1P6%a#iv`yJXF=32D&COH1Q zpiEtjdN=e~&$eb~7g=i5fDk<#xQr6Eh06LYOrf0o4!7$YJcl=7=jBa3c6~=E~VTQV6DqxtT{nb#R;gEU}U4UW=w}wSF8;TT3$<*#>EZU%npLs%Q$M^Wh=GPK$YC0XbMw}$ROnCL$T}hbT=Gf9XGv=k{iFI znW<=M{fnZ;Z8^Z5v=Wo4xWtrDF?N{?hO@v@>Q|z%^pY^(xle9)7F*ldQD|3|q62Fw zwU80$qOWH;jU=stOf^nEn2Sbdp)o_C-2lAG zn2S`vo6h#H5+6c*{q2C~L5S74`5DgTlx{cY=XjNikpBxWvEqgfiO#Qwb#X2Arf3p+ z%vrvbj!K6#3b1ISph6xeSP1zkYk5m1bpD9b$EeA)w(ay4B&==_^FWDC#E=TA+Kn?+ zid&5TYS$J4C9~1iB5%`xoCzRxcF-I%hEH`A8e`417AL9q&5Bc`{C_~_6qJ?U-Lz;6 zreKR-LJi?8Ovp-GQ(;ZP>z<`-$s?H;hfC+#S4G0W?90MGJNv5iqCxick>Z{DRvN3J z7w(M|>7VuZ$+404UZr+feG@fZgTTH|T^^@T?om%0bYD+eT1o`eA#`-W=HWeFYsJ<3_Zd=<+OMO*OebpFn`^`*l|8UxR{Qe-m>e9if zc(D*ff*m2t-|?!B>DitNlE3ZvPGs+8ER}T>Jde}T7>}=)&>x12=7{>jnpf6n!B3ZPP2`Uvc?VP1?5b(mM#Z+>s%AOfDqchS(h9ILS3u-v_Z9)&|Jp3| z>8P1HO&?R67p?)*t9rWQ>6VH`U^%|)#+4qzstZw(2P&erqA$%-K0a61!@>rUzf-y4 z#M(r?->Z-Pkw(>+kZ41?{?C6gdtBHUP<2@0`>Q9d=wzUA{55&km9T$S=rm$qj`M9`Jd5ZTPa-g8rkRmhPOIc7xC2zJ?l&DvmV zNzjD~w#JmOJHovvB~343e>l)=ku50)d1*q*WU!4HZYYR~E1_m%xX@*p4w|S}B5( z5O(%MZTk3%pqSw-TMiT)Z~4~8a+b`H?yjT&E#FKSGcpICZdl_I(-~#iw(@exL>!Tw zC&jk7>psts!wD=KP}-AIs?KjFIYNLbDQ-b#>| zj2xI4GPXiIdmvpK%>4!hL8AgS5~ya(@(Xp3Vgt%iKQUAb0oNY_Xf>GNjJ&-W(@hGe zUo!->mLpGIk(5|r(!|2-Bz1f?84?yK;Db{~G1ehO)nrpD`OI5@IgZC! z5Z4@%zL5}(;fgOvkO4vdFUBDdgIu1RkoK!efaa76Kpm`0-H-x0gk_0?uLOdV?6Hwj&sPVYu8I(o> zv{6uNLy8G1t>6Ubw;<1+FRTX&AR-)WT(Ct#iwu+9lu)RV)?h-UiXhDeS?y3(1TM%Pd~cWo~@75TMiGVwNEsTzba%JJBDRfJ=3%+w(nJK-__km z13%r(r=9OaS5NFkk?M?Y8GcpM1LY!LuR6Y? zEt_9a~w+0&Z`>z7CS$M}Bb(5;X>D<{11E>Hfk-qkI?@4lO}D z8#s&sDi#9R3%E}y1MHFomrAtL<$6YZyd;X50Sd@ii5JA5fFLq{M;wIgf+!-qFNt(b zKyU|uL5THgg@=s<7+ez42)8J3^fMC=4m;F9XeNmkCeVjRoLul0m@N8+?=6RRvH2to z8R#?{NyctqV=W$uelFeu!}>_lkT5p%7eUOR_|^mjRk-=_Az&d2iK7IfC4m#eTgZ}~ zb_sw70Q3R@=n_yLHd6&+ePE6;Kvx*{fJtq3{}8XbaHCOBKO#B()eFREEq^C_|dAD(PTS zP7bcD=fDb-j$%2)O!ZSojTUQ~AROm6a7C$7WXc>F2}K8#<#S*$w44Rb+h5{hpbjjt zmh*fj6vy(0lcvg-Q%=MtGeaa+e8W)eGL5@5RYV6DeRVKN zLI)Dn6keUGTATAYhV}Uzm3p7%t?HJt> z($!nVGYAQf$Mr@XSi2k8H(4t!Lu)tXO}uO{M-(xVfJL{Z>HhG#0o zH=~m9HNuT(#2zY!A}<)8YZz{KCF!e$`;AF>C1L(IeD5gK-G^gfBkKkS*%)zssg}Xq z3Gr@2EY%QSA+9+V;zQ)&hde z;y_qA_$5O>SXwT*Y-MOf8ayPxoPVQ0scfc>QcU7KcNmbGIbRGMRVL>)U3o`D{L#tY z^oDm$d`)P{|_5fzo|wRs`S*>b9`WonwI zg5|7Rh820l7)xg`s_5z&ygXb$2em2en4mPVIaockysx=*VXC%frcv)~o~lhZYiq)n z3D=Y57;cqTQN^*$s^c4$wJj|tv$vdRiBMB0Yl(y>W4J!LeXCBeW&|t4cB`J{Tc(ng zVfk+SXeu43fw>KpYxD_wLxScjU%L#;3|7ds3j0$SPOE~l^)FW=pdk2Ao%JEiPIQCnQNqmEf% zO1U?qE|plL8QVVC-ENluUST96;+=}xkHUV#u%jVpSPfz*AuCc=Vpt%METQ9Ec1Zjy)D6&KPa8$%2lm`h71!%gv~) zwF)Gz!Q?AYD;sUKrYNkRsZrinU#PV=4Lgns6E~*S)uSlCgBVkPG09*=e8pGg*;0Or z$<|3pc2=B~PhI62P;kO4)!pmwK|O`YZo6~X#Zr`CN3~x}z66`5mOY3ocgQLqWYI{K zN=lSown%qmV@j#8nCPh87oZU!p>qLA|$i{Gt^mN#&T; zQkaTruLyNL5OYIBD**I_NtV(~C7pbnZw)FnC8ZPj1~RPSlwCE1R4C(kRi_NE++t-+ zV;d@C0%9cT=yhFXjg+lMg6e=QM==$`smZAG!PQ#Oz7!YZl2E^qFA9spM5s;{Rn0RY z5Syb+i6g4l<*ZOK8kfm{%h#zueN&yl^N^RI*vnmpYa*eq16J!Zl+c(d2zrM{7Q zb2s#Ub?^2P{%d@!M!&u(HmcQ);J5pplKV@y*Nq(oibz}LT6NqO6eNuz@};^$5@|G^ z&%rHM0H=geOk@CfAo?7JQs7WL6cYp$z{_j_^Z7Jb%K-$zf(;G0PYkyEFbFd9HX{5o zxMK!@c45GqBybmj;mRNpz`hXJ(MP+Y5;O-GH6qb6kR&rfZ37e-20+dr`WTF)$5DJB zpnF@aFdPakK!A zNGJk-kwpctuq+aS#R!xIgcOyEe38J1qE)7Hj-0U`JV5TeN$n5o3Jr%*Qlcz96@X1kaQfP{S-JtEwHzB{ znRA()_Oqo;B(VHZW_k{)Qwli`h$AsIi1=pe5SKcJ0&pM{0H5>DxhtJ%0@V18lpRY&olY(w-M5|#$$J5%+E z3EtL&P`;9=j+|E64Be3ceZ#mNgRoAHuj4V38PP*B*Px~(U0RzCl8x^Fm~JhCP7AZNo*Z=~<*0y`@IF!8(VU;7 zXNYwu#tFw_lN;)kH^x~MD!$Z)tio1$GA?A=*XJfVhbLiAd)YvA)mZBECr|=vk8%Y( zf0CMW5t#;0ZH)rpttM$^hFTc{xL|_mMdH$QQX(n1$^%-+iBkJTJKEBxZl*6_3?tq$ z67FcYjMK2_FhP$bCtZ^h(27CkTsbtGScr%aO||LC#c@i^>N1*fCe&X81Si;J@@O5< zfVjXI#7?pP9wY~dmL)O$oY@Xl?&ex_Cb16orAKD8waB|1RBa3hA2gVgc+p){(K8oz z%id%0vTn33%U6zL(d1$2I|)gQl~NWiWGmd`iYx0vMMZ+023BRQ*;!7GCet=ZNKIM_6%SJMP%QQOo-zWixY~n+j8Z7J z=EVAm1K(1maEmF>N+{T|MGCtc0ryGF5cM)3;ZGBgjFFX|h?IdwGA|2dHcHxRToA`F zAs37-8kVSUoJE#*_Ba~tLLLu_xYz3lV@V5Rv{kP~Ejd&EAf(>fhO1RwEwJTujjE;F z`lEuEZDvw;Qzj54Y1>uLGhIgoiPzD*z}$00_4-*)ZL;}Jbm>H26R#Riv#PG;I2A|J zs)lX5)}vw7SedIPI(NMV6C}IASxf=L^g<9bh}qxt{Moi|sv-B=o@10i?muvGQ&6>w z{3Ej&P$c}F_2Up!yS&FmMBMg_HB_yy_Qe&Jiv$o;}M zB-s7pk1t5?7k+pVe!uX8ixd3CQ<%o^7rrBf;xE2F!SNU0CFb2M%<>n0dW_~TzV&H5 zfAKxdCi)BC7ianlFV3R+3*VH&^%vjVB-vkleF_^{yc`tK{qyEEjqjiHz^ftrix)}? z<6roODCJ-L*~K~k;`NtG`WGKcN!Gu3HKmZ%a~|s`?_Ydz5#oPN^pL{*7vHQ*>c2SV z0G~E=J`1t`#UoiR`oDMy6z2cutOM-&bOnH%4IxDWK>V9y8UW&tPZI%1c?YwC8Qtzi zq(pEd9~fEB1-brQje^)2r@n|8W+Y}0BZ4@TE@cWc&+WR3^h|6ay6Me`Tugv-nGiLC;Bs|bvx026%hS%N_8rj{?cjxaHtY=`uIiR z$2j}S$mome0fhG@^`}oez@oNisX!t8LBrO-%2E8Y&zkKN|Lqk2m7w_hI*;Qoyly+m zzXX!MaK^W@{I|3G|1wzq!ezdl=AVVA*iQ40uI=qK|Ex!7JI!B!9@$Rw-%j(-U=(hr z`EQFfe_=SWo#(%u=O15m+j;)!kJfgce@4u(o#&qwXKv^D7j7DC=lO5v`DX+<+j;(M zlx!l9mjZ$2LMI}qhDa!yeOnPRQLhEz7QIhF)qv8*si+HA1gssw&7-Gy~+XtITnL@uW!IvB{$VMI9Q`z{oN~Mt$ z5d_dv!bVglY>ZGZe9n2Fidv1csmzE0bOvkQR%Lh3G?>nnei5!+@KBJDBS;mOQEFxn zk(Bd@)w~)+JoG9dlh3JgDC1G|vT;f4D!!u-n1)T!gEHD`O;Lb9Q=J+uAchLmFkVet(t~C!U=VHtSKcJW=~qj^F6jSIn`H370(PKz!9Zlo zu+^F82)*ir%Pd))%qi?}>D3bM9sS)bD8nsw>b04HkBbfI*o)zc(_^7qEBdYTbc*t} zZ5(G=dYKo(iOO0D7px^4kG5K}*xhkShypm(mZAC#k(;Cp*TC|&geCPD)x&uwqXf(c zbIg)2w~GU6r~(cIABiB$gNt zY;P#x1POLFGQ4Y;Ha(n&G^KaK;7yd%Jn2xumKT2{tz-c!L6+$pY6H%kk=aSgE62h) zZ?28};F0hULp8CNVF(N@+>YQr72v!^*sl)#r#(M-hK1)(Yj&2mLLqN+375&$(uR&ZLlkv4nr%Aj4%nC8()l+~rIZ z@oc6#L5Uh6(^PWieEc%>lc_MnU6oDaY4wx-mD&D~O=(mnfPbMVV8vv26jof~+-SxQ<)`R6*c?xXf~o5zD*m zBgEhUR3Q<^m~XbGO2$sEKN{|XnY@+&-XDdVZ8CzJP%kOo7;;s%D%j=exTdGFA{h0V zMyVM`(ZL`t6T`uR{BCd(a&maMSkamt?%Ez-jnP)Aa*73puyAQo=+Rbj9x%CX;ajeL z=bord(^j+!oL*_!4a{A;|69oJvP?^>dbaLV9K)$bG2le73ru?^F+N^osGqIF_zv9@MIt21@tfJPd7QMhL4VKA$v;aOcDSI_k{d%0Fk z;72V<8}&19dhECN?tBl-)^3bX)P?ulG^)0xJDQU1!F4UyTwdBIj&eb(waddjWY zazLNFa$Mqp8_90Dqy!_`SNHF-;EJ@sH2ZRq2W$Ac#2QdvWDIuXpyuCd_Rv7tqnV`= zJ1yTOfWTyJZe@*I1>RwunF$U}E#?)Md%TQURLmX#t3{ZsUJf)}o2O+x3}ApPt|S}2 zS(>`BZsMNX6c}xdFLE7ZshrSmP0&#hR0SA=vb33*uI(iIXAHO*{PZ%8gcto-S}_Cx zsfC(176)EJq0CHu&ScC`uW?OJ8ojaJ4mYrcQOYDeY7l(1`#e*dqrx{Pq&p7}2PT+@x>ox*u$X{h#E7H(13)_uyAMVI_gtVKe z_h{};7Tj98V}lUcVLVZcVRu6idhR%DrZ?sIQZcyu5Dlc(&SqNkLcs1LoE^bLAF1r? z(s%3Z>q9dsh!Oh_sNi$@JFNORFJ1%{1gEXh+SpiYbbfrL-C?tpO<1seYG+T=`8FPuOIFfo zR#2IU$PMWLt&77tNfP4CQcpQYEyDwc?Qg2L=Ni)lf8bPDx_?X^&{T@p zL7NJm6erc1G19W|w$h0)I*Qq<}Kon5q*@gNtToCnvaA7~k?&f})v_sxlRlxbQ>_bVJ4sy)&c*3^`bn z_!(ZDq{1Sz$ukJKSZ4^V{|5-Z_A^Az{iV=gmKO5T9t0ddY+Eu3{S{CqLz0Y}3gKex zZ;TyXf`lX0PK~j9N2Pl_wg88tPM#8#OTg=4$tIkz8Q3i*Z61akyl|hn%h4%AQ4rNF zO0n6+CFWAt@*Z0i3T24uh-{0?ZeJP-1=f=1d47906th`;6-ccFp7-g} z^;Eg~?YQ2HM2)-G+CuVY8Obt!DIFBRrFAgL4X?v;Ny>z9(o6rQ8ZTo^QZFvqB19L= zccv(D=2d$Vy?jfl+Nn^o)l?@93wKdylWaA{S$h(Vp!XcGgyEtFrTNhemEQv-E$xf6EL-ilG3r1t!pR8S z*M@*0rey0aR0kc&yeJYWWM3AoVHE;=L`#d=*Y$E8^&Pi(v@4n%+NWqeC8toWA_0d@ zd~j`gV!$Hq{X@te)_u>cdUilL*Nm!d8qTt@hfQhruui?jEc$ToN29c1!>YQzr`u|b zW^Lbz8_jlacTw7KHtq*z$f1NIoB{%nML`l7%G=^oj6nh!%x06~xS~{A=5IwvVL1OA zCOnm8r3ewh__QUABLp+8L9V+b^_Z_IU#d?opOG!cbC>XhT@H#OOvA%9wm}{@%*8_} zt}qiBWbHu=cSIaQilC1XbV1U!jD&|AYaL-(Vw6>Y1t~+gg*65+4lqgu=c=KgVg%S3 zE1{MF3r)cC2%vlo%uh$OAaGS^P&)$V%HwiC47mxWieESe$eiU)DROvo8v*Z}o9nq;NMPtC_RhjNj~-tyKAVh`Wo>LLq7*&a0H=@3LX+IB}DQ zdx80^AZrsPO-m4(S+w)w1`#aG0gClN*zh>IA7r>o;J74-+=3;~J8>e9LqWpraM_8k z={`^ZZ9tO0f(<;m1%OmxB_=Zox~GK5#WFHBZwrCnV+1UWPtOE8^A!Lvnh3;k zkOiajLHaNf9b_%wKx7Q)EFr%jA$fp+Ke@FenVbMqA&_AK$dM~S-VqyD3bLs|&OMZ2 z@oB#hhZm>JVsaNU#B7*6h;yGnj6B4VL1Ofaf}6xRqPGj2A{o&l5zoM3_g(< z5GcS@U2tM8PD_&EdgJ^sh=3|7YXFEp$W<^A`5swaCQw8KV;6!_3lL|JFAyZ55dqhZ zVTlU~T@nd7BHUgW7RAA6jIe&3jGz)+Ye&@~BuIe(c@`FYVe+04LNlmM$5t_uXj1^( zXH;{EuQS8f0|C?wAhkdVjxI=|#fY)E+=zra0j6ccB=&>qX^`*@R~L^7s_lo>Bbk>( z3Ub+(rBoWSFAkMnMwB$GmcfB%c9M=bN}F>~wR7zTT{+z?c9zM%9q>k@6x_kfLl*9V z?fIF`RHNBkTyuC39`*^jN71so=F)|!+M1b0y|a0$Hr=ePNnRW#?9R34qi(;R;Z{A{ zu_}(^R1MQvyII~-&3W86c89}VgnV7Ahru!RJh9XIvn3O?Xud{i+5)E|)`WwM19 zV2(e`*@p?VlHwA$>HfcUB>sGtP+9KvLCM8gJ@RDtCx z_$ot?I7;bC6`=M6DjbC}XXJ6LOIHYUMk&G`iDeBX{_)%O=?$@hM z=H`7CnD;qsq>8m|B!~cLCW`Jdx3#Pj?Sht}f53hB>MQ%#h>IhJ35L>Mn&=CuP+hMe--@0@)Q2t`2h zaiorGty~#5jCOUEVad_jFv5S95s+I3I0aJPX40%BII6fYaM`7HJ~kP$o8}=`5_D7C z?p86^DT$?VB-lEZlHNv!PyxapHP}#WS9LEWp?;^Bgpvd$fvVtU34Ec1st4Ns(YKZM z56Fy)NK*KKAfsZsL<;ENRK-D9j*ghDss^ZXAj2!dxu|@A3*O#~l2%Q1jX*%WX%+Ph zWwaC|`;3?sa)KYnT*aK&Nm3yJZIoiFFU!$Zg*7ce8a~FGmJqF3EsbH=Wg5_?DA;tC zMH}}s+mXyG;ziOC9J`x~91dTQ%%jw|n=@odR@!861J(H&C)TFxQFc?}Wf6}_cRa1? z`MO(iJg;hLn!a2-CR>*$G8vgfro@kj4G8tBrt7-ps-Q3#x@FPNq`x)iCTW>chbkzH z(yybZBY1J+7$s<}O5~*g%io@FG%K^gkg5JSUW;BZO9}E^U5~!l^$X~}E&5@#=@V)vb)RYYTMS=ogI=WMC@wgZhs`CHX6AO?{$qgXyIisB}aBpwJLZVuyYN#iYSj9s7e(IjD!c?`)(x*NjSMx*k*rTAPB@pfXl(cBmmU*_f?R?lCq+Va8;A zrZGM~U9Zg4C>0o|yR@WQq*uC6F?Fcv^rB)L8ZO> zIl+&ujMm0aY|uCNeqD#kYQaCKkl+7hG;>qbXs9;=WhbLDnC;fs*!*nwlG^lSZF8+X z-l=q>AsPX_p|Xi;f7A1$k+RUk)6p(XOhmMH`9MAldXwnt5dH~ccv5kJ(#oB40Y`3E@$6JlkUdobkK8YO)ZuA z!ke!e{zkU6cUpSui1N^s7*Ke9dTUnrmTa_(@NMx*rtsp_o{aDv8CWCXds2$pIimtl zyIJ9#@EZ`803Q^bR_qr~49et4Z4e2+LXuLDmOu)>Ck3h?ygntN6<(i)v=F`lMNg;T zp~MrLfR*KJ04aH0&Yk0;n^Sww;?K@1*@|~0tl0prh*UVB_$f}Qj*0iiXfs^=W7$-V zM5ZSE+zezH@E*=PQhRtg??`D43Ez>5mJvS$86{BhP2<8~(*a^RKRVoP5nh_8Ifxf? zqPi~rjgW__jALjL%_3fLT*@E-h)BgVYVrOC z5l>U$T;ekr<0GXNK6AF)DB4+gaY~&%FA$AX&&7`;y8hW8oJ1n0T+b`Lg{DhTeTy$6 zX=A1ErOBvXP_W}&7QLV9OLi%7fA(PFTJX|^z2d$f*<2D(WERn{#l0~w1!KO#8vrQh z3(F>frj9aG1q3aR!-3g)@gUfespiJFf{?JqfTYd2b})dG2P`WpX%SGg=?=gY!_YI9 z0|VzVgG@eT1>A9Vhy*+fkdMSPjxj+`6uE=q>LD8jKmn9*1cBKT#mIw>LR$>TH3S4D zK}ZD&h?)c>25BI(m2feO0N6qUXAS~#zcRoT#6?2_oB)C)K%H4WdKUL5l3*-I8Afd4 zTDpOja+sZ&BmoDU;1`KGf1$8D2_QHs10iF~ z0y#=af>lfN1p%EaWSvtb7@e5QNDo{C7d}CKRbkLDxm;@38^5YWIjPMg)pF=P%TSg15I-IcD-D? zP+l)BmMx5AUN|gHWnZ&YbNl zD^`;iqdu4x4WP)rj5`CazEz}7WT}+F4;e*>yXi5>{M?HRR~$UWmKf~pJ!x_whPB`+ z3~0*$`)KfO6Yv;FCh~wBI*>z?cj82DX!eded>q8r^d1B8MWOqp!WKg4G@-=IAqIlY z9t!mssJb$}1azU~x>?o<6oixQT+{{x+7O$qw`&toaJz|WKnfq@1a@AuHwP4~<~ThD z@*>$e70Uk2%db2{e(8XS!uXcPm_^HbSVa*{PXA-M1vX2YiOr3rSlcG8on~0DDwa^K zVrhvot_(M3JgCtkglx=Er+A_kI*6>CDerzhd)fz;VR{A$_hNFUWM+bIs!RJJeLPk6 zLeIEZTVS}jgnPkgYkaZ0|1d^86jMYrLq#`{g1vTYg2328_k;1-ap?^cD?5t&UXe#w zc;{$oP#a@V+jH1^$}H{VV_lY5?$pFql+LS5MD=X2TA6T9@9z?Gx`W3{mX&d=aajKREN(P!CV*9&q)uY=E=uF(TJY@O?p(OYJm5s8mN_c;5i^j$-c z<;Tu=K^-C-I|#}V;oVt%5Ep>H0`BiDwMRt5h%_6E1aCbea%{A{S1D4fZ=ytpzA)fB zPc$f|qyA%);`?q|I8NWvqnkD;@;r$i{D6U8r_i!XIPWpsNAf;AOtVh?SY6Be+0guS4yeGNyxCnlBl?fFu-}j zC_FM!H7JP(HC@29wNTMANa=>p+{T5G_>5ajr5EJQf~xPZOgQTN015>%#Vh%=dRUbm z(7c8fmnBut5Un>Ep+Hcj4m8j}Z4M*|s<7^vgz1f>76PU=1}pDz^W$##xJgZ^PMl0v z3MQt4=Q5YCAPBh3MGq>$b+NEkaAow4s;`eQt2ER1X_<0O)73T4y^ej9b8`532#YC zcGi&g7S?qHJDONegWZ3{d!PJ%3EJ*MTX-^90;~;Ic2|zq6ovp6VpM~!0QtdGbq+ra zg4WgGU=y^f`dc2iqcuFra#~jTF;jF+st?@;YgimW4mjA$28vCs{@ZI+A{}{PQ9kG| zj^z~Qktm3l4YN!|{3?Y(auLEf2oeY8%25I2z{L>sRsxHPB5wioHG~vJq1qBi-6D8u z)&93hT(Wez00F3mW>}Qu59dkzaiPU2IHp3&Ap5GsQ&_^W!#Go$HLCx%PVCF?tf+)p zwvIL|xrC|!pwc?BG$(*oDM7-98KL|Gnq357)6pbkssJ4(NfnN`rciOaMn0tz;s^pH zc#PnR3u>YKUoIn0((oB%=t@(_WfjX{p(9Yg5mfYJD+OY-ZAj#bugJ(362LT1FaO8>B?hht4lXW=OU_G3Z-FXuh<+KQ zBn@joA_9|ay<|{DOF}{z(^BJ$_=B32GW`QcR~=PZ1htYSH4`O-=VjEtaj|7g>AkoUm8#1)VI{K zs>PCB$mO^GKDI`?Z>uvkVDpAv5ZQfOmRof#$8#!<eH}T$X;=K)+czeEupbtW%n64UwASX`}sKoC`rPGPul11hgzblpO zn)7QC1YPm)C`iHE$E3(zxD zsoHtlXPD)ja}QD&)Hx4Pj1ieLqih9<1o4}7tC(6Jae8yk>St4UQ`Jmz4g|iQNebsX zXA6?!++(C(REs8!1T6j;sUjQVx1>{n^UkOgx_QponZg~<8CZtCM9vJLXHSB}owrc9 z;wxz!{=A@yuQrmYQg-I266F>&%DLVBd86%A(!J9NkQG(l-^>GTnNROqBEp_s=4>L;)& z(j`0<>@0y|^zul#V`cNAA%V2nFtUKR05{S292WWj0`&o!#pF5ooPNo6VAgh^cF_d zi!qRedjOYBhca{}0gfPm6(=2_FmR03DS;=%2nRSfK1AOTE(DykoewFL$7SNe1rnfD zkQL3L_Sd(A0#JLF8Y(O6g0Ppdwx|?4;A6`Yn?`i>F`0YR1OV|o zkhb!M48vj3dCbd#udL43cb8BwM?5Sr5qn6;9Q9n_dvxHw2#8e&?nnZd>pyrjCF6hl&74Q3f42TK5G3hW{2Cdz+^3RZYv&E3R%-b;s40 zOAnxc9DtoE2Qd8AJv9JYp|x3xPN)z@0Q7ooUZ@FR8CA>mOz$p5 zNYDY<6c7nVRnUkMHlLPr)5QEEF@I0Ql@v5u#`uGvg|CE@I?NMilN526Ow@rG^w7r5 zZ}}`z*uEMz^oC7pB}tj-+)|aEzf8YUS;97844%s6x7CnnVbk$c?z zw*~q4uq8I%n;YWKD`BI8&I-0d_B3dToRlX)$-TA&nb`(eqP7xha=I>wjqy)&rAnEL z67*+Bo$CckA;RT=I%=n>P1rC^+D3F9YzbK(@uFtkump#^sO>qgD#GR!$2U+xM=88B z7bYr)7QOmnM`F3GWD82b8is27;uefz8&|w=i`S9S@*%tbfK*i_8c6K=C|Wt@x2KpL z3a}r@?{vboXuPurcc+lHDzo(qc3`k>E#AhGX?K9FFRZC0(dd)xQbCPPqyxim^g^8} zr0WfLq>yH*L=y<;??DKHpfZtwNvZ0SOSIv6aE(%$RyiG`&|a2(QD_wiJ}G(xWM7zJ zSE?c!3v+8%tGjlXhBiR-YRcs=*t8F>+K3E40oGxWkXB*38KYVz94F%fIv;YZWC?W; zfbWRoazPCjzNipxUJi;J!BRDl4ZMUb0H7&@iWP;`nPly_V~>vl|7^Y;cW6f_Bp##2 z8K5WfQPS}e>fDRY7aeTznrxK@T}3Y#7k!cG9xJSA2;+9F3AHRG5Wv7X$}DiUwf;KF zS;Jam5ElVy-s3f@Bne6^ECw|?(E?b>F(*dyWmn@sP=f?FMgj*RrQj0ALRc$Of5f#0 zk_=DYccfOr(@qP;6FI>B@mr6opj_Fx>8S9YO?b zPBFPSP)t{^FoM%A_<%IDvIg>QQI!iAA6I@?PeL>gX#}Qd=9zpvplVg6sKT1DqG-dC zTBM39thQSD4^Ze5WaWl&o7T#~HY4&oz*9XtN&CTA7dEb1vJ!8BLprT6P*tYE4j1&n zjyzufN8q+0#Zxin8H^?gC;FQO9y!-V-gl!P>txE8Y5=_GO+L#@tMmi(Hn4EMnY zQ7<1tBtz`SQl-Pl*87H_Dp9RCUlph&@QNL6wWbKzI#Z)sNPVH!-W1YT&yUa;H%bb} zKDjtsZ-@JV$`ChFcI|AC4k&&+xB~%7W3f}G8Z0NEmX)bY$RVT7YWhlX0|paSpKrHj zYt1f{W4Jt2(h9~})YuGw=fR#zm5?qgwD&$usbWA_MbTS1AeT(TJE?z`>mtjI_GD-s z02U2pfTXH{StZEOfqIYY~$p~@}bui$wOL1;Z0%u_`%TV3MH|&og{B*3t3Mg2v zB1Vvm=BxzNlcH#z(TF5N3G;TrG`A6$s-(L6FrcI0m^uWsL$hijC^0Lkz`fEmE?ACM z1_+ERx~>*_84tsZ`lf)G=Tuc~y*yH!D=Y5UtHg~I$H~QF&+H2$wZiPnlI7wuEH|^I zBLS!!Ljq@g*U&5~f3x)3Q2J&){h99Y;Xx(1FMkt#O(=n5n3h-7%!=cgRnM@NO#sI< z`H~{4nrHjpQbSQ+S zan;!n2tSfg)GJMNN~Y=wtF&P?ckoDN6HL%*19rwrkWpX&0E%&AnSw%WYzPPom7r%rz9O>qY=p@L!vs)%g@mjPA7>5l7*Gx+MsY(h5hx=Il<<+| zCQE?c0GtCXn+8GqKuR1RMUdYm8<k!*<9 z*jHAF$6TvSOa)VvBFY>m3v3Na~uH#i@4@GJ&2$G@YTIW>BSPOao^4ED3#&*7FRN-rOnFZBMBO zHHL+ikav)Pog$n^ZW&4@xkw^B6thPqwL3tnMFc{UkHZ;lE%HuF(7qhXyrNBE6Je&B zO2q+_%2KL%1Zh=lv1%@i76&zJAo@8%FO}p4B+Kj`>zwB<%r*tf6c12PKvUFe&yCRu z$JkN;1zL&$+cc+~`J8JlPEyT})nSPj?!}c4n3bKB1r(O4=3Y6{Yl6PLjbSj(?$c!X5Hcr(BEsjeFO9f$NuhPZ&0+eViLluXs_MPd*43#y;g1xea z3mtL>H96mzqJx@OQ3$D6Y3fM4-A6rC!PBumhOk#g8!fA1DbetV>3?!hXU>d7dnpcE zb<$(Y-3ad=BrPE!6PTiOA=*(F>ooGtWV_XxVLSu4Qn!TcHKx}cqUJI>Xx#Jz;?;8{ z!FWBlMBFn{urC%W3};`IXbxmw8qQX-uNss{)i>FKnkvDax+zfh05WR3W%7fMph3m? z!Aq|V`N2m`Qam`{i4ufkFA5pLma98e)Ap>2Yx-5o)B}F(@{oizDUR@Ln+i)avgp!9Gkr!&6sX?ms&yfIU&o~_Nu;ZS+1>uWo#ObAK6zFt(pVt zgsE(-Ie5B$(mXBV)E9-T#<4uJY8y5!8n;GhXJJiOpSa%>*H8uuo9Ia(W@C zS$2d=$J48tWob(I>87T6b_pxz$PV3z{)*}SL%32**V3!H?b1@w^{Pp`&7)(j_?c^^ zH*3Rvr8Y}-H7a~g(->;=!X;ukRo9{2MadFz^k}TbSR(7FEIv+4Vm!W1LVp-8mcv>T zwG-+4TT_FRi2|W(E?tVXeE|ixpJMi`+aLbna{Gki zR~_5)d=-npb8Rga(C<6bpdZ(2#-gd-t2FiXO@t=wHUxV2y!Q=b?b-s(DGin)AX*y1 zAR1JvJ85Y`==V?8DL)?GXc2)B&+OZtB z>U+UMvOMce_mu2dv^QD3WvFK^!`?(l?Lwwg)jiMgR4hQjVov!w|;ngZ!5 zD)ttexzLdMGiS)ooNwE~s-3KLmg>(0w^(|{j_FQJqxUjZ@d&fge^p|h^B1S9RSnAw z&2!or5SneHk}ZpL%w%n0Lw&N|jHN8u$4cIu4@b8&<%aDDr+n$tQ|H`r99swG{NVV} zpl)#+Ue%#`vL21*BSjf}J;a!$I|ysUDm`6*p_{`k$Y^Pz_JF?B!Uf`Lj&{o@>*2 zF_@gO*7W#TjRKZtbX(Ak3V|Inx8eHS>!}k)QCe)SBOk|`>*0_h=a;T!O?K!zW@?-3 ziGyGB-C(*)nLpjGx`ydlj0rR=diL2LnY}}%Yx7gBPVuP>jjlJBmLXiXq3=jty1HXp zdRcSnSXEcEZ9D(T*DExLtf`DX^#``+XF5}jW^=LlE-)ORie12R4c{$m6V-xH#_$|b z+#Q}#r=n-qDUYB}IB;=OH|5TW;&V6B;lO$;(i2Cgz0XgbLer%w)K|xe?^5Z>_f0+j z$k)A!?u!E2@y4E=HS25h(-c%SUp-k1ps3Bc$;J4FAQc1*$JyUqE!il*HZ{{JEr4=- z!wxDB`4N<0HX|XF>Zu%#v2dd0yIxcdLgltfv5RFmE-PE2&M%ANj_>(_6l3As9+en~0!fGX3DM%^0zc5LXv?RXt6rZva3 zTQjxdd-8B_y>;vx1-4{cceS!kHN9&2h8Gm2^CEodYtt72rw`;iFs=(*~e4FM^qK%hDtS3gkIiUq1Z z^)t%~RH;@=w^$FViTPO*?dyW9DOwzrn7PncB5A#RxU+!gImGi-+gMS7B&qg~LeCaD%!@DfN1z>glHGFd}lh zVr1u%gHrv`1gUJsaST$A@o;3t<8aT2f*=(>#->*Fea{b2Pplx&poN&7*{o~Q@|&pl z&)`fbkz7a^8modd-M0MFDjhBzfK&$XxY>Hd+Ul5m>W#aWG8ZDrR;{-_7BVASigPUMQ0v4yU(Obc0#x3co2EP2`4S z^kmtV#))UW3SE`C=LTKn18P(xoeLoaWuma*fC>j;hHv_1b(c-6>Y0uk840C>89Rre{0mDlCc;y3p0TFqyn+o?W3@PWF}??eMK{Og9L*7Edi?`h9}|_IHCK zbv-Jv1~vXwvF#d!|8{hTQAec?53`P%KpVrm?_gBD>0a7GB&S>VeNA5_)9qHNRYn^L z1JWXx?*P(Le3A!aQE|diG+Y`Sz$PGwBg%{&+SSlJPrl|+(dNn-pmKK1RLZHi>QV%WQY#iV) zBRi_Ku-H-1@EeRy1qMTNs9(G)W}T{5jz!@kqsU8@i0rs72^2V*PP%$fuPhE57!I;< zpzJtWP@yYjE)D9+Ylg$Zw40*I>}MAWr!X|O!i^iv_Irqd6<@xtUNueE0HE3d6+!^j z7{WA;nrzjZjg!=s)P_Smvj@$c#Q5(M)#34R!N3992KvC^Uht?X8lcn&nn)kBX%RGx{*S zij`f_?8Y2HrQ?;g+4|hPituANJ{47Auj`M!`gRYkYPqHrLWbvHzFg5==)!N6-i6Tk z6hwhKAsEX_1gRQkQ|W{qO#2o3QJKp*T&X*(PQ#f~SheeoQvr^S`c(m-WHsz30SVJI zHH%*}70myzW~zAdhlA|Teu3qz*_Kxp2FjrdjOm8To{T8ZWJP(j2qu&uat9;q2X~%4 zDUWuTvU>kC&HyXPZa*91iL5OsoWS0|`V(kLPoU@ex>XvcNAo?+VKhC>3hir|A?^|Y z^wo_M8`IPELe0+Mh!hc)T+Iyp(@U8?!WH2neg`nys^rb1&~W@BY_(Ai!GaJns)l z)p<_)NNG&Ds$ozA+RmRpt73_uxy^+pg_E5GAVgvYhhtJTfqIwyxf&8nL$5j>JyI4c zfU-4l02O4GM+a67fhuw_0iFJ+igy=GstA;omeV+?dm5@wi`rd+*-N&&R83`Qh=r$; za&lcSYbw2}86;XRzr;ja8mq*#ZhCgI-f4`s>NRDhj^SwgmAfMKQ^S><*Zc&@s#=C; z2BBN-^X*pXLCHLkLR^s}n-lSwl**QdV=KHg4eCW&t3&LGvKGe*G!b|e5mQ)O3c>JI zRYrybERMz3E!$fi>JPeIr3caq8p#YXGb2aMOw|e>4yw4EnXi{t-Z!)=_27LDmf5mJ zvlmoQ6wO^|OodSznrj5${nfBc=~q!G$)S>(72C{95Y|nk;OJzMgyFCh#kf8doy&sK zxn9-M4P=M4EzS;mbgMZxuVRPokFWR!OHX|ey{@`qfu~XH-r*~nP_$-oXlJT!!Ek)V z6-}rQemoVY0M0k(jJHAFp?EI>u zQL(eDwo9C+Lt5>*#ik0%!*E#rx^0naqeIm*ec3mVeDV}UMvfoCBZ;7mwwnuWT-OP# z%%*FJ6i+H=9t=)FUoqtS-GKvEvz+$V+bV$$KkjX$)RYLUm7f*o;Jq{ z>&Rqn0+v&;1wygmu1)4*l-mt@<5NNwp&+kBP=%xE1Vd(UMQoqR6)CI?H5`=+euHIL z{wg`W9KRYUW#mhBnzm@Vqg1`((>)kff4LUX=d>LwNJ&;*^lMX7iptXr&a~xeKT@ z?W2H9oHKQnaO!%ESp}0f9FVI1tm9I&tzh9T&1H<I4i4bW>$5= z+_HgOwq9`rQyZVk1G={BNyeD`%nrxf@0&RV0NTD2JT0ps8nX-v(={ywToB&~%Mjg& zFpf_7JqQi8e-8?QAVo>pz~1nRi{6=TnWA@^ zt+{~`L#}L~E1Jgm&Z}%9LnAD#i0)9!s4Q@jOXIY{6PZ)2wdf~gw{Rka&~>ytseYi) z)2BDm;@nW5;@}n6u1Usnjzewi(B6^?Jiq=H5_y< zogTg!)>X)#cmcj4Jn_l^4#Oc9pL)Xm25(vwQy*B#P=>&rdD|Av{GdUkY}yCID^6MR zbSEJ3t%g~5^s4UIR;bLLnKOD7$6}wJN7v^H721ZwQc>**;*YYbJvPNjx)x)PW|7YY zGuS1c7o4`xSX6Y^EK0LiNj+Gvn!azlA@8QF%8;p7-0WHIs%5U5t&Q$UAG=tbm~Hb=}(-J-GTa7ZfjrM!T6y3%!=v?{Mzr7nNiQY^bZNefM^Yto}ksjKl02UoZ> zsq3^Cl_&5;x3}AM&hKmr&a-G|Q^|og9GMCPlkd2t2`)NzRioxJV-6{>%Ep3q3yA4n z%rc4oU{ETi&()kD%3TdRuIpEQ-3&!w`QaZin2b zkdIMODs+Eng}aOdMGmZTSHr_^S@cwS0qd0;?z#pi+)e0VONP0F;i>T6@*UkQt#oBu zR2$PdwE#ioDqIUFUcl-PNmU(yYKf~vF2U2Ql>P9VU%{DW=bu@TDK9{J8jP;r_cG*p zI=b(zk`9dP5iqW+ZONQ;LOu%YBLjw+q6r_4taxnT(FZL*);T|}aH=-t`=Ov%crQWNJe;UTce#YcW-tmoX@Ux_iD4RYcw~v<_jt*^~R%2NvUh6 zQx?6!)&fp4fHQDy7S3QpqnRj(6@YmR2U>g+aA_0JS7VLFt$Lo}`J4dRtx$r_?SdcE zYR`1WYtwZVq4IE$g+pcAAe3rZu`i`YF|j)_Qq;|^^o9#v0$&};D7ZgR6>`3&7vMx! z103C~(qk5a(d0s=U5^i$KB_+5YAdv%8w^&7Lb&gj<}l#V>EZJycwQ|uEm*0S=a&#^?)M}19YlQ$Sqe|s)6cP4H0B?;*nHbL5!K$3a;t*2EeXv*Ag zhnwtS^XjqwRbKK6Eql+dq zMlMx7rM-bFpqUz$L2cL7x4td6s!{1aYV?*KQaKfR)U!{vC8Uiy=k(CX`k_@3f=FNs zzDl5ahDQ}w(+IgzXC91>QK5P_r+AibyCHQ%4u@Ab`!!5INCsBJtOr8kz7w{3$}Tw2 zQWv?LL`uHuCO$O6;(?vj6^|^diEL5M!v;P%l*?vU5+Zn~42R!BE#=-=`!R}qOWF3_ z)#A_TSyj^r+o9&&0$X$of^M?cJb<|9U4oyz6))y9QBff@mJ+$6PR8Y-b>c^G!%sW-j*L)5)(lsk|wq{M* z64+)#Yks=XEM^@w7>^<_r0Ka%S)~PCqfUnJGc4e2;p~NJxy9!&G%7_%ayq{x^@Q`m z1y0qm>HoOs0@oFlXJU;(wdI-NNEIC9j&23zfpQ*dqw3S99cb|9?Sx*1&_Aq9Nhli# zpDM!v!#{LzQJ0sG}3agi%LdUmKrEg!qNTXL{pQptfY10o`J?A#wex zWtcWY`87D&?3|e1*`yodV^cMs3x1xlt6 z!?CI`qBHH_9+ZXM@R!60@(bnk9Oo6v*EJjC1yLKl;V6+$@*TajiK7~ZqgjmOk3cVi znOjn)DLQS#!6?a}Xz8w!lA!C+lce$Ks2)A_0t4SjIWWPaE<`!$4YPRD*f6`cXk|5N zT-|hRR=2?Sx!r;zS|`>PYil;NI#VYWM2imwSiDO^ZHE#yaDCJBJr3^32#(+EZWm!c zqFR5FkfZ*1ii7N3PmR)hx~hcj7@V@SlQV6hCZU4q8w{&3b4l~)&?ynkZ5h7JQbQ3G zpTY$^1y!j2m{i~_0}1Ss@NOp6b!}?BdwkG~PB;k`>gB-p{7h%6(QGa%q!{!^s{kxa z_3WV9wNiKtSE~kAzz$oY*%alg-RE6(raBm5ac4rxn1Tp)HP8`UIu?9A5NKe)V*_0` zMvY)Yz1?n{sD>OC8erjbi*}*lyk7;WDj^;`(+s1*GK&Hh&AKUUVMm>}iphTth7~#H z3Y2VZ-CZT{Bi*i2rHKxr+&T9RQ6XljGG4e46B>>>-Nk^+uo~u?+I5!i+9BSlsPvnC zghr|^MFWx0pn6-lQ790h2gO|-o48f=TvH33o|&6C?cicCM`Y(#2PSr(cp~0yFCD?{v+EhAK-R~N(vvO%Kn9l5=E7U?iing&}LY1`AdqY&f z9dR{3U{{r5f0TV%ZZk3svlF&*OK>&aGt7Ks zv9AkfF({-co4?_pRCIS}_KdRZg4{ro*D%>g0iQ}!RzCAnaaKPVq=Ka0vjPX>RdEs# z&d(*760brf>g&czR7yJIy`dFu?V5Ccl_spHR}D7|%*cHBsWpapMHj8A4#uSdkY(7x zy)Uh{M^Bce1LVO#qD54VI20N15?gMSDu!Ojx-O?Y5`^d3XsZapO_i4VaEL0z)T!mYN}5xY zbr6Om?D;M@j5J|dv;;Izm=5&^R@_fJQ1UD5C~~QFX@#=NoQ>2oE4rS2qW7hF4r@)+ zPNe^CO)1z(hhr>GfcG^=(!qiil$y3>@sTgSC7jG~II46)Ljx@ynFevv>Hxq{_NP0J z8Dh$0q5^_&AUhxE0s<7J_JbiQDg!#66?6?&UX0VUs4wjNOtE%zdaOqiWg*<5R$FGz>SOcC47dRG?q1+o7T% zqi5sudN!&8yx|ZP%(w1YrD>;8U{Bz^u4Q+A{m|!>4;7>u{Soz34XsGoOSg1ybwtPA z{JS5-Hpe%&nlaeDU7@TYvc8q zTC+A$Zx6rYC>ktnKd)K(b0_p!sqfhL?`hkr&#f7yN@H^@m<-zvGWvFXq1GNBZ*=C` zjnVm~bu##c`;HtiM? znAu@=htVBsJB;lxwL^7>4!ttE!x{6B+2KWJK9R1V|9jH!uKG#)C$Ia^9w+Vp%)6fU zl!fpA_dgi-e9ixW|IEuj@W{DG-*aB`Cm;UUZO?n$fd{Pp-b=22{Gs2x^EC%ucg*+A zW9H_z9C-8m_kZ@#)-%8V{%<{S&(~hR!;bIz#G&uo=f+<==ABQS9sBHESN!0C+iv{D z=3iWYi*@T$Pal8AJ1!vr{J#nXf z<(KYw>*CZsH}87Y*848n`ll2155M8T(?7fQPe*RO>fVX>{+E95r7!!_)<^ER;qEuy ze*X{ey=wdqH^1Z52Y-0e&kwo#-c#>+%iZ_i{FOcKzJJS8zj~APl#5S)(WTlh4?Xdb zN4D;9*FJmy?c@qqo`yT`Y- z-m>-g8$bWT%PYUQ?oH3$@YNme-LSCdDX-jn!(|(OdeN8PdY1OIJ5IZF=c}%M*)AtP z{G0cjddk&LX_`O0bl=+#YCZRzr~mx7JHPwWw~yR<8^o#(xC-9LZpp)WPG zhwi^_-#0#Xx`ZL z^k08AfAdlO_OmX3`ZcfAu6ym?-%+g@`JU6?aP_^rZfI!NwZ8O!@7VIIn{R&S`sY8q zap&6M?>zX%MeW=h|9i^=S3KrtKYr2OfBUtut&jWn5A1mENA~*K&Rc)G^^q-q-n{?A zdp~`T+g|vQ&%F5DU2l5(y2E#R_O)~GdE~x>?)c$hAK7@qKAkJ;*ZlVvW+tEZ!|5v@ z-g5Ab8*kfu$^Pd&_`|W?e)ZIE&#$@so3DTP`S-p5YrDVk?CWp%*+u)D>OA?PEB4=V zU*%)hUqAVW!~W;|+a9+5c*2)YIrzJG@3i0d7iXV&_U>2O&DS4$+k5^n`@q@FecyG* zsTXd%`NaQv+=jD`w4OiWziF5M{Ipql!EW>VCx3auPtLpGu=76nmJ=R4@48>?a>y08 z{Ow_TJaW?ESGbEeeCa9g-}T`?9BIA(v)A4Jp;u{d-T8eNq{bPT==uL;; z{_BHU|NPVcrCBhZ{Qeic=^YEFZk_nWar^(rj@O?#b;++^aq}nse8zLWbNK*T?AVKYI7XfBnr1IzM>OIP;(Wzo*ij*?i-5FFB(zapd*8-u;a0&Uo_Z zabvT8f8{l=|Nice-{%v*y!z-roMG>A*JodR$*UG$`isj?`Q6qXrmjBlhI6;>dHTlV zcRT)}XWVnwlmGt6g-gD7@rQnV^y2f*e&_4<+VAQE4nAkUFJ64fFV{b|dH!v8{^ZlQ z{rl^FdiT%on5=HxaQ@?7_%A;ix%}jhJkHzy!FSES{jUGK>tlDE^qeDhyJG9dj&XMW zo2TqE^2*6?UHj7Syzn!R`Sj(F|J7$FkNrRGBNrcc{NwNY{l?K>9Dc#{Rp0yR_(MC+ zeeaqlfBS3a?)$R(HCz7cF<-b~=c^8V;_LqS=jScne#`Hzcm37cH}3s{+uff%|Kt~) zwevMU-}~KPdHjtZy=bQgpZc3`c<0~tyPa-+)@ApceeD(dpS$Oe*WJJSTRyzsrPF_P z!rNZ{@5f*H$P-T8?X0)lasHEb-S>mXyzo0W9`es$I^mBye&(&ayz?a+Uwim#U%hwx zJ?6m+J0CG|=Hg#l&${xx`+wz07yZlb=U?~G)i1v6-|u_J+_%p8!ss3E{=r9X|CjIX z_4`Zaul(=__dEF4n@4{AxBxRi%)jHB`Z*iF{ZIG4VaKn|Jp9trPx$%uzxaho3*c-*LA;@2sg$Ui9I+%_EP0 z?{hDG&J)~ET>RKeoUd0ec+Ng|&HUs8m;I*wst4cpojqUuyQ`jl(WwWHz4}R)e(Kr# z+?Wz} z^Rz>+{=3POwJqQON3DI+Z;##WuDP>syZCD#egALY>L0yR=fVqjy7kCCUUko||MMSb zRj$7Oxj%m4E`K&I|NC1%cgrW%w*KzQ>6(wVe^9i?|wegAf@8}=${=1xaUGt(ZTyes` z-1x53f2Cjd;!l14W#|6KNk8~#=jh!(IrWh}?%#OZtFF3t%a*4;q@S_zN3Z+l@AtlQ z#{>TMl-;iW^J}yZo_u4=b;n;i@qdo{$eD+4e#&jX9e>_sqeuVx?OP81>?sddUi+Jm z?EC419`}vkT>6^5&wl9sH_a_vbc=13?HeEb=DCMmdHfw$?D~$`_DkRUyMH+Igby6!oc!+Q zD^LIO8R!40{@DM#;8mw?m_63H;j+^aty)zEywXraQm2$A@06&e5BI<24G-V_tiO5jgLmrZ-2d^{UASL+=lB2e-1E=;!u!5@_?rDL{M>Ji z|NHB|zW2VHe{#?Fzw`1Np750CEZ*?l3%o-QzP5Ftxpl*R54T_Z^ivP{)x)p(`pcTH zIq}O+Z5?vk3oGZ%T=@?t9P-_-z2=Sk+P^+@^9P>f?0o#iKl{)T?|$>UUi0nKU;34~ z-!m)dOB+9REARaN&S&4X z-;Ooo-FuCF@rjoodfhGBzn=ZG#Vhsw_xYz?7EZi%-z(nqtEc_c-FWMZ|90l3pMLD0 zu6pR=L%;d2-}>KsPCMnXTh6$)v-@|?n!o9{pM1-Aj&GiEy6jF8lCb?X=%DAGqOZ*FNVx zKltvAuleRNZ~N+=H(H0i`Pc9C9@yz0cY67ck9+L&+>?LwzJ&|__nGgwW1s7O_v#OP z=Yr24__YuH;e!u<{7DO!o!a`>&wtFl?B{p<=L`PrryuYBzde8cw3+|Mv^xe@)^$;% zosMnWwr!u-wrwXJ+b6c24o{qPY}*~%R`=J>ci+1A{ux!f*4%rosy%;9#R!$ zM8#U{S|BjxGEfCUgyEx~Ic>sbR#D)mnY-!Aa@mD_ofkKtJ?UwMtg9|H%%Ih(81tF} zdo9>`V_=JxxZp}BCZF`1eC+3)5vLab^?M^}!kUm+N?Z*v*tLNPTEBI<+z5|MBx5$3 zEq75Bj-x-c({5x$&oF?B|H1Y8TLMbAFvQ8cE)wpRJ5yT6{g}C{wvRbbS}ILY#X<>b zoLmi?C8yo|H&HPA@Z}*O_@eCa3wo<_Ts-s&XX*5^h!)wl1%YmmIAF&?*^sbhI3w%| z2k0Y}AzIUqZ0>4q#2XxTtKW13)w{`von{mK<+xfq;=*!VQcr8(blX zE+FeQ?|fC1zOCsY-81PoRYCgY<8Dz zerAnPHEv~9RXylDs3jJv%YbDmUdF$OHe(G#R#G9qdU?`<$ZSP$-Xe!EX%dDCyaWgK^qFLP|NUo!G7vyNZ*t zGa%?pGBM-XDU}w*P*Q!Ptr1`~&PkcZn^&(x**qcn+O%v{7Dnw#o{|83c7Yrw_1 z5SUgoTAPx;Ot#u!&=Be2v|y_jK|5c!r0aZi@68Aid1czY46gb1J%)(`i6%PH3#Wmu z_7ck^{HpL630!j=Dcj)SN6O{YGWh;NTKHnQ;zm}bt0+4C;Dw|`#|BH*`-wY#xg3Ax zroM*i1{H}K>;uc6i7rN?k#payxa_H^4c)*jI>~iLjPNVhB}X@PO8Tzw+6DtUoo%T) zG%Xdg3NG7n#CyjQP1-3(J^L)NvCep1O7@EIsRjdEy?-?e*^5n34F)v&c~A~L+_Hx0 z%Qi`P097Y;3KtRyvo@8QmgAPn&^4s_mrQ;SK23sm@~D$NW7(df%O8fij$`Af1z!mF zXVzFuUU171)w9yB$ZeYsRjw4L2k~ZdJ9{4Zi;h3pH3&J62$9w<&Q0{ZAwgysU7Iu- znkoZ1>QKX;!QU_Z?T$qCcpyRWqm&rftfy?P^K~iY*Og?c8?UG{=(=VVdZU7AKULMD zHU(^W6q~*jv8%o?nhzw|zSstXF*>L z{3Sa#?0ufwEhOy91^lHEMeL-jxH8D*?EcH_s)3nNNtm1)xjoZI8Fg!^Pstc4=u7#J>2+x{xs`iwh zG>9d4Nk-y`_^8qk^XQGD;9m*8rfXx$65lLLS<;5qt=XzJH+2ZL99J@cszbwBO!}Q6>F?G z781DsKm(XaijDqjK(zC5oQNIVVrrD0h+}LU+1_@EGPq#(8n;j>+yii8HXwLQu|D*% z=>$L{irkLE&DtgZG>vqR_rz)0#=Sjl)x)?JkM(;DkS*XgdBcLGyr zGR-S0SM<3fd)IC)-E>FHrTr~$8M%Dv5%&vb0p%rMN;tz8j0UjA1+YEI*^a2H5)%P( zAm`{lOPzGpAlNzNl_ytbERU`jsHOoPfDj9E#flVoKe6HB%`&UiFQ=LW+1%tLbc9kc zrSI8O{X4<~+|>QQ_ZhV(b=DncXVNbdS0d&}T6ZGq!Owg@Srf3wD?a10ou1Y$U09Kx zR(AULuQ~#f6jueTZOc6ITP-WnMKTSVE#+|`sK{4)Cvr3(eUy4hoC@Na-M;j@Ijv?^ z!f)+@ZZ*cXb|7mOHgmYO)&j$?QuehCC5E|V6;r$h8a?8{H$ahvcpVHJd!R~Kt%$&1 ziP%4r{1R;s=ha&iGIIGe-L4z&Zv?uw%$A46J?@kZ1k|w;ZvcWwJYl zyL8BqyEz;{+Sy&h?b}t^%8WJRZs&^Sps{)$wIK?A!%8fE#;G$;0}Fzkgk$al$ht;z zksN9m;qlm$fDQdT*&y20%L+I#uvh;=6&hBH51O=tj8*japu!?4q>*Zu7e<7oo%e5yq6?}&J(Wi98zAqs<`)Q zhD>~X4q*9Te;HiYri71e&`MrwxTt7Mh+$%H+b%NLg>HCe^luTzK)gukva!%k1+1u-f!$lCA(3fK0y8cxB_2w3zug}(Rw8P8t zk9_;_M-PviV=bOKVKN8uA%_MJuUh#+2)>o6YPX;?R!USHs-22EXIJ1Nr^caLikbUgYR>~DrQ^G%J=hBMqrG3X$NRGNI|^~ zQw$AyVna^VjI}>`BeKp0tFoer>KqZu{@h-|U#e&a<6NO==e@KH>?xv+^FuQ3pEgk! zcPL8HkTaX2@(MH(*>11Fq^uX|zMkj%_ARL`Uv?A5^rZUe)KMdwDx97G>C-#&Om`te zWu@higx?)#Mr&>)1jR0lF{$1X8aqgX_Co$+x4_&9o@sA+r)XRskMwPkuflfC>7}aUA;!r2BWnUSC!~>(IL%}H4rJ|t>cv> zg_Y>MFX+yS78NxSSJLGb{M?wSLR##kSRRHB@TRfCyehF1l6=hAU7I+%N;rEkwx-Wk z+;|)nFuH{A*l1g6m8K?0G2X8%9V27)(WkD_+^%G|{lUQ?2F>`k_nZyh$rZyYfa7dK z*wgj~sY`!`8ChEPNQ0E;RoT*B`S&-TzWCZ3hV2<9@>rcv;iCzI-(l5PCI3JmLs}vu z)YA$fFiJY^?1;-I03ag1ioZyj%M96h;iYp?o;~8zBo;yj+nW|)I~;<$PdzuPYcA)MzoI3vh%RpaXT-_CMtM6C_mfq;K}zG*GRqC%qu5gY41dgy7@ zmGW?2W6_Nw@TiD+uL=z!?Rzt)k{H;cDqh@zF$RHuVww{W>Omb3K;8G*E9V1}Kf1+t zc``?x8LbQkLI*wgrX@rHyQn%a%~zhH;SY)(bXh&*+qT2iR9EsFYO-l=aS*zIVPQ^% z`I5W(Cx*bW|w$FoE{OjpZ%|yWUbk{Q_peyaF5SG?j;Z zc;d@>sV6@+5lCBK=bDBy(&(>x;HWWGLI{b;@KjgdSr~7>mVuY(v{K4wTtCZ8E%aji zuevQ6X6zWKx}M0I6XAr?7;=zZ9;wGtsm$ol0`$s&-NoU>z^3{_8c7woG*x9GDU}KF zI$f2RVjy1Gp$@iOIgK2z$yLoT>lN0iTOQE(C{r`R<|tDLPbogYJsSn7?S zEwh9PM(=zfg_BqR?Dy>LpSof^`G$LbbYUf8Jal0XTjD&QuWNZHy4h6+vzztJ(`sv6 z7C2Q?Aq7US2g4M(5wWsTIJ}jJw7vm~=V06%QiopqI+4xBf#{Or;vV}OR@C*1TYsf_ z2+b$gt@UR{H`iO;j$UD_h?Kpa_u%M{5(u4+*UD?!hb@k>j>K{%EXtHe;XM2WD$O2F z?RkzdWN+-`ZwLfROWRU*X61Y{v|NxbRcf^B_N`iDHV2>;C80I;z%hlC_d>1eeX$gR z6Q!OgdQ0tcO<(c6G9Rq(^2{oMubTTGD8pW@2UQuK0-Van0Uo1jKNs$Rg8^$x_&S%r z8VD4mHIXhfB3t9gz-rQtkLO>4gA_`0pw4(U!Yc$-=WlhJJ0cKP&|kK&T9H^y(wP4| zp|9`>*ze310r7^qTQ7z0zcx=E_kRx$$5L}$|8E${GT@GYf!J`!7{}nTd1X$#8n{5JD(uwan`Us-U+Hu1p0K&lnTGShpo4 z;j_hDcju<&)jA(+>KmDqo1Qj^{d?larG)_@`=UW{#p6c9a?xk#GM`h(X5K%~f)(q< zi!;3TuA)ZTlt&WHn_vCrKNK=?h5J}|xx3kK6O2-s`w0$?a_{vpkc6kdJ{D9@TjD&N zljCYLUSca>etSobCcSu4Z2+QjeT`1DHAXz#+&qFjsJSM6ra~3VVB#2DlhU zp2SK#0+eG9eV9`;`E5{OolAtXBv$8N;tZ044lEh`0lVyotkcPlGV89HmL1t0@)uQq z^`|QQpIhAFTHWz6zShGW?Q$}4+~A+m2#e6fn1uvur}7!6V^liQ;Ty96+gxl%2^CKc z(#ch1YLewjGUbQs9po>HQUyL@l}ET?*cSEboiE289FT^~I#e9=k(XlTrhyojKC@!C z@PZIL7BWLUXL(6>V$KkELL{2i7caV3erVJSEp?m@#D&cG&l1_m~hUG#^!|cBs>>?%_S>r8h_M zA%*hhVVB{uz^Mi-2Fp|R317H$1s>DV4Wy3TwrK9 z%dn{&ivMhU2gDkDejH8&i`!J9{B`UW?5X+Tv6%MRcsjHlc75256`n-hhCt_ILbXUK-T=`w$ijLUVgv{$vQ7qXEWlpw>lR@u%ki z@tUQ_)?pVaP157}iwg8Il1lQ1V9d@h$cu34GL0)WkPkILvNkgBiDOr{rmAZHV!8qW ze7Co3W$H3(EMS?tY>o&t>XLK8JVWfS^4vcJC4#vc!cxP{Nc|Xs_1>DhDD(8&Jk2wU zv--l+2u!`~*Cu<8BJMt}_67S0-=%(5DoQ3}E~6%QoTQUhP78V&yc^?q-UaWEE4XWo zZl(HX?@HX2QvZ6cJiTsM1)td5St`Ru7gN9n$n{tJpgOiM}@_; zKUv4wA-Ze)ZrFtUF$e2**CRweMWxpe=8g7ey@wBL`qLDfF zwiu#Ur89hiQPHrCMey6A`@z;@p|tY?JAq#n*KY3k3l?q5@8(Ijx84l&R_}!G=WdFX zoXK2v>wKukvJ`tHur^r<$(ArLVnsIyYOII3E{kq)s+;Rpd9@GNLFx5Ss&rCsbQ9b4 zD(`Eq$A`H(`6f7(OuZqzyjFbNfK3`m&op)51=GtXd>Ycl z4CPYFzJmNpUVvr$;jl4%MxMtpYb3tpvDA}{q0j=^Ee@MQ4id)%G}~tVnO7P4^uEl) zlqHngCX*wkG@ey`f2Q5~v^1ZE$1ipPHaI<+`qD_YrbdBPXCU9L2=a8m#}t)wp(=D| z=FUo%+3A+6mKxgPtoXyIeL}Hwu4Vr$pL zox#?dF6kD-L}w1pH6Le6{3wQ0;MCOdwIBM?j2wCmRTD>Yw+GoGk^ljp?fEN<$KERo z$3;kP%DC7ag<8+$B`i*$2THb$cpS2N;Fhr8u6o`Dlp4|FgjXL6!WinV!? zB1NzfMxB$BX{;tAMa0q&h`WQd&Vgp}skw^2(-8-w?3rV8?#EAF7wdPNJTZ{YhOwlZ z>wH!zjboA~`wy?%0i|PAVI?`fuK&+oXnb2eY_s9oa~k;-U1A~khj)69T=%V^@L-o@ zl0OqkS(9WNm{I66?-x&SeolHO$Th{v^cAxuxf?ww!xHlyUTY)K}^dqg>QsAzuoU+XL}XR>I*F`>M+ zMZ>65p2=f>n2IU!|5T>O{pxU#NBW52$Wa2vi)9M=+4Y)SeVRna=WZKL9P)-)x_+;V zSU8g=zJ#o2FlXWy6MOfKzFhr6TETpea^ zp!%8SJAIb+wD=Lx1BW=b^3?`aBHS7sPFD0`0eT|L_}hlAh_$3mtuPAYMzQl*3&jY9 z`|rcFShB?T*xufN;BqPrHO6rNe9&sShveeB*M5key?`?-67}lUQz6K5>sQQpLmb>r z%W{?lvYl}*aJ`+ArDRw>(;m`t%UmJ-{FA4JNCU&t@6eG8sI2<_6GM*$#zLek1;77K#r2K zBM!-}Pz9U{1}YjoJ^d~$44Al3%a&Z^9mVgt_q+%)@JxB1 zd3pC9A9^iZ-04N8O%~DO;qMcrRCXG3Mww$$3J+nWVDo@4>lm?6KqTChA#fmlyECNx zafP5$qH`yMt6`c8W5aZ!C>yn)uolc5)rkTRk+2Q&eR;Tk6lF7AI)$cpmbxo%3+cVN zUJ|8GSuCe@iTWpcb{bj22v0OwLICRMeVTx{H_Jot8szC1)eUr=g=_E{UN^vRz7a>- zgYBLCT_EhJYkKOZMwQWQ7CgTlK6E;arZU4kNnNk9rtYdE3+z}Ysk%~jGWAbA4h_>z z8#@ppyeOwtx=rlI>GW1^AI*MH8pKGg(-7$)bIN*M5zSQpuRitNw^r3($MaWL-aZl9 zVgpuhsCFlTtzOBIZVot8XXbG;h z?HAY&Uj2`n9Q&cgB;2ruQM0t#5(S+9}PJXKT!SMV1> zIGnXZbWMN5J0FcA(YrVFs<{TN0%km>)n}gH<&g|v5g0nXMAD%@abK)B|3bYjjr9fF z{?yr$c$G`Q5KF{L(WJi7wb=)iUxTAH0CxN0WTyW*P!XtB!3XD04)e&-;S0gr-IK+P zWXuy1L<|s%RvdYW?|rWJe=@<8)BZXM;LwkAQMz~O{{PSH)70pzn;L_wKdEA%!6JI2 zO1kWVFs*P(rLQ!MM!?5<{V9aqX*6Y!sk8~C8np+`+`bKw|**1>`032BVTx@Bd8feVz}~_&n4dmUs`-7sJ~Chj~`YO z;Iw$ST+vt_Cqb(K!5Sctn_qK57}WjccBH7$lt(3B{w9EFmsyqnm9&!}(vt2Tmnn%+&@hw4Xuhiw^CaI_zjHg*a#MS*I zSLmz>lBdY^u}tvSD!dPG*%q9XqWL_BNO(6A(;+1vOl49AoDZSOSPfEH@0cVK7}g({ zeIFT+Ra#`U_#x^rSV}Tx7J$ixWtNJnyyK%^(}}QRH{!{gU<$1N3_@ZRNY2mynCu7(- zHg42(Q4x|b+@L)Q{l;yXI!A&K9)L$yB2-W?GS7gb6Ydo#P)j))owt@TZGZ%HOOi8l z7C+cpP`>jx+X7Rz!m^$k%;j~eV9y};lGuY&q)%|u!901ja+D3vydWu&KegAH3&lw7 z4dwVHm|ZGye7_qAHl(!1QCy@?z!Gn21GF118_yvRLXviEOMsesXu14ZRMMXyhprKx zFc~)IlJwq|KBW`8u)S4f!yC3`obM(E>|LM2TsK8fQ5qE3%c8%JHIjF|=-Ho+9{DM3 z{$M3!|Hl#yppK!*Q>lJX0eG7|xif2NCtPL=Z#;VkEvaA%u+{8{kLkKn7ZRW`4j#-Q z^$^Y)&X1{3PhgH;pGtTAG=C|7yLiZ(F;+O_5j%u|3map7Qg1KBY+Dz`vSsq{h6GkKgKHFRvC&yczd+mj%Nj{zRE?N64o$b(HtLKy^ZSYYJoh#doo|W#1hKep8a7x>)UhX4w zR)@Gc>vBQZielD_0fUb-`lFB?tYIq+4CQSFQ6f&|qA5Vt2BJ%P3lq{D`RlS}xCy$M z$ts#%t;wAUM_GaQeSwGvx(~S%RCwA93@mll@#b9^*=pnsX@M3I}7Lh2o?88 z4l<92(W3Dx*R7zgSE4@9gHIiv<7&(TT6~ zE)yeBTxhp#35k^aj9_k4GY%iaSF>fQk^8g5>J04{r+($NM@##g9VwcqT~?MF0qk8>PHAWUbkyNV5d}61)Z8@Iu<=LSrf+jBG43z+@=P*sxYDhR z_kRPTO6>mfQLMtbN3%a`$5*4w=a+RMnRNmC7}Ge6gz#q-QdM*QaMG|Ds25WEK%CWg zmixUH5f*tdW!;Gvm}u0x_So`>b=Y`ToML*(*(BOzGWsz*dfToqdLk{xS`rYRpO+r_ zRmW&8qH16ko<-Syg_M)WJ(ELat<`99TIcT47o{->qo`J#wQB}=X-uP$7U@o;v+NkQ z4{>H>FzNhsjdnS1!D4O%8Fd}5OWtNzXL3|cb%cw&d?>hTWq&1g7cGj;RkCV5=Ed|R zo%5WrbWHdktB*I>{7^g(uynGJdsBD~$Q1heRMa_BB%b=Qrps4Tzg7dH)%2=NAdBQhQ7Ue&Xeoi3Xj zwens2ez@BMeFRRX;kPX(;P2BF!!{lDPZ^M#M2_sYkd9cG{4W+jZeZVRFLRy|-VoMb z5Ue)0;2m-QO4jEiiqPf82bi%@%dfwU1~VQ=NL;X>9q9}%^fi_+-t?K$GQ0cTBJsa> zJf56ByzR^48C=SI3)GreXxUaO6^R=F_b?$ys3wNA1zL$l;z9M-C9f-eCQPwY>XqdSnYdc<8oz~25oaC z9f6js0y6cmdRnUy{k&B0LB44CfyXC@=dhA^zgoDcup9#4w+y(@dY04w3l)b2tlIeb zTgDFFuHfw_>4FY*28f!2y>u|0A%*lX?~CGd+vq^tw!jaI8sd8^MWr4Fl6 zJ8UKD;W9jHKaLB%P6F|gxuFy92?~3y+z=fs)a4HcV+mbI1k~ltG5Pt5uQM-L>6Qp( zr{y{Jjgi!H&rwRYV$`Z=UK+Wb{&K*Lbv(-rWZfHf^(g)I*dwQ{oSH-`H^1gXenq#s3Jd5nJZ1&is`W<0H^UATCb zXRZzQ*H!88#N|V#X-v+{9-yseuHxXi>zf>ur$n(sO#>_Ui1M3EAf<5Vkuznr-1N?J3t(3YAQb>12u=^ar|((Im(_zlBTN3Qy) z)21ur1_CA@loy=X_rvz#mbsUmj*jDD zxkiuRS-!Dj^w9YV`uMhn3&yYo$zQ6NM1m(qPEIYTKRb#(VuefN zhDuh;=p^5dr#2}Z(#T|8swJ_=wO0_IV!~=CQn!Dn9#fDut4GU|bIiSVf+zkI#ecoRH(i@X#^y8QU2$WeM2qG)O@4=lsv976r^AvRXU*LSq8|d}d~)eJa6B%eX!i^>g6J z7G&2zyjOYm*+ApTZIYrictp9XEIkpwOD~G!M9V(X9lADvIj6l|jLX6#N0{bA2x zzvDHh2C1d)Y_aPfuZ{Jcc8odIDe7m`YODQhCt7l7Q2X^6-kx?$M z%@0tU9lCi|X#<2^Kx;Ak8P*z!^wRSK#u?p$MrkaA{rxmwm}I1zrgGye1>R})_q3#d z@03mPwDUB-L=qfRn3${IY2@{F_A4BsHz9(ySt&KArO2hBe5_3!YL-wjo7IhVKBhbv zTB5A1@@?=XoMb3OBdDAk-k`za8Gkp?{zwOF&*)j~#~&XjFf;ujpy1Z`SfvbEeL6-u zCL2m}fd)t#fafZ1B%_nqBlPwNYnc>ksVwJ9rjq@o z;I7t;QQyiTt~F7ms;FeZYk}aS*8o2ZZ604(FOtnc1_xIvwA?cDBZp60jo99hd={X_ z`p^U^Ng7()BDx2xJBndV6_8rzZw!wgazdK8B|z(<%P!V$nxLeYsB?xAY@K5Kg@q)2 zQ}VSy-UgH7Sg3uS!qEt~A`-lY4A$st&bDgld7RFk-x{vNO&f8h7#j6w11JhbNFk$BM}-f9#-E;sv`QJz6ZAnu!=h|t;NK*v zNTBFPllUvP!n==K#$58Y zNCrc;qUhd4!*3Kft@_6;f}_4|eXR4}6EB3Q;kU!MxUP)JQ3GPmLrRKWE3Qc;V(IQz z4itbmGu7E>sPzhVrK`*I2ARBa=q~&`FC3POeLX%$$YuUH0EX3P&HH`wd`*@65TJWe zWKlop6|z`J&$kCUq1ydJTxnTzTYZ{y^^u)$8SJ4s=e*7#RZ(39NdtIL8hdOIz9@AP zchLAG@2ak3Q?=3P!n{4g7G#*iJT3+(w-ZI-rF3`_Av!WGc38Ht;f-SEg%Q@<(zqZs zUeX$9FbK^33`T@@RGl6wNG0f-bq^uH+c`n1KVm0Mq-W@ez0_c@4FriR!UY##b<&9o#w*(Sd!665> z&lGgEd^;=2HY=Fs{>aSDy+;CoAI>kL_Q2LCt?zBArouV^S>Y-fU~{GM;o1nWu-F1O zL(^370&=eBPv~A&_Uhq)_nCTjzGVV^{e)4urxiQpFWx;O~E#WphJX)h5wO)zyxV8_#_$PLjLraCHId$ z*)Kb!zCj6NCBh6xMdHK^=bfVg(CxCX=l?e0j^L^;ZrAi|clE{E^j6kxcZpJT$=ujR zQsvFxrMYo0S-0VD?-xhH5UjoG$()UzSj5}?<*i{GHy*OH#^7;Q+{h0dQm=GJKH`q( z0V`j~J5!1#Sz%B(=3c$8Q{}3m6&(#b9;D#hSzG^?jl%$!=VfSlv0v?9c?@=0>ktxlxktr1ByQ|**sA(id1#^%jK0kZ}k#-j`s|}VvTTCkO_N9 zFH)!X80^XRL_DBAUEBvUq0hAKl)?mfl+6cdQ@Ois;%nacriM^zqbkv>Vc_LwN$ z!z>s)Sx;hB@P9R3y;)|qQnCl0ve){HZH889(#2>!tBb*7602_JLkp2Ys%fxj{0GZ> zsRQaczXT*>>Wb)D62Cvtu*~i}q-jJFVlHh#Wm-vo-v0h2fSIMWLEZqa_x*vv;o9xe zINyPrkn_Pk$TI1rL#ZhC8B`*F|5rggf-Tpv2%#_mR$n~BG%>+apgvWO?;cAGcXU0X zB|S$4gWWj6%5EgEE2;XPu?cL?&5p-ge;hX;q8!bpv6Mqpylwh1^rF6?ScSAh;BBr* zdn=G%j>cE*5ftPr-tW68*cRO@Vrnz}t2m0JnxbgLnD|V!bs!IM+)V1J8xU8CnU1V1(QJNy{#4ia0P#Q8FM< zkh}@Fa7%UOFxRf!+3u1t3G-~5Vp8S2T0NIgDv;45SH5^mx0vjZ877@#6@_Wc*6_TJ zg`Gx2{iBHHc6Ai{F1SwIpYf|=EVd<*O+(Tj6@)YEP{*wz@FSoe%y-MSrV*WWWrZY^ z1}~w6Vh@)P$KogkMsyV+q`L7yPSy`X%9RC>W;U${jaee_urD`xd)>)0rK@p8(B zDhEa3u!t>u*89*!BE!)=>IJ{S!iZve%YqSTyuIY+rR{OLAaEMX1B?NLz!p{1S$O+0 z&V0_(PU;P#*^}E$p_`OlsL^|PVL(57L6jksVM170GzBE?L4V@;dlH_wLEH2Am4|R~ zs)T$TB;yMCJqp25(zSy?g`z^nk)A@!jr&N{?Z~k@eOC2+BY1QD=u?~?`I>#vB-_E3 z>HOD~iV8RdghEaN6$$%cu0 zkgP^Z71)vc-T4PLP|eoNCs2lfZf)CdZwj&I7P!XQ*Bi@+qH-q#?xYcCe@VBrIYAgO(p?P8r*~xt?^Mkc+Ah+g!(QdwD+z0(*4&t#` zaDye!mklJ27;uLZz=^|XR+%4z4Mo>-(o}$rj6x+-7fc+Ty4Lt^xw3(D&A%}k+8+$= z@nB|SKb?M;_VZ#23NH~_K*(LIjx08xPAm7oN2nHqnM6=-{fqd?llz2crTLx;#O zN)oYzX2!Y|=0_(K_`o>uEWL&B2uea9OlmB7f$5UG2K{M5c9Te>zQKX%%(Z9NvcKg= za&n#v7?kSHtqw*_J4wNyg*!F;%UAaA7FW;x3*$g+h{av32OZ$luJqtA&I?7s(1mrr zKb~al-7Rsbu_kY86Vp1?f@CSU@g@8|^y;7LWBAa=%AV)4Vf0hoqu$n`0Kedmqob?3 zr%r@65YC9&rf1?FGsP8j31;H0B9XgsUbu$Mcux`XZW}jI^#tbzdTGD{tTbYxI5ll}>bH0Q6##CXUtX02AMRnF)?rFu9Ua5oQk1vL zFIH?XXKNZz)_hMKY8J*3#1GD(WTtU>sjN@I*9c@^xy$S zdQ17hXHbXUj%iekb(73Q>Id)l=4(Vr1$2tPgvb%T@^+3i+4Mq-e;8LWsu?s0U0vre zS<1@~E?ea?C5xPldkIPcmX{OPhl>Xxs^az*>n2A%bj(im{heXC;_)Qb`Q^fSA2WJ# znDm%^3bd8=+v4&F$fcZZz``ZbCg=I$_&59K9eCCv-#*}wt5|}VNB)>gG0C)dS{yfR zp$(KJISg$0p~v=v;KS&s)%&ss)9I`)J2Y<9A*VG&9~?c+tB`zB%dA%>y}W2`W^y9$ zS7zeUCK1?@=KYwMxor_Xqib6H~CkA+DF<6EW&6VRu|S?7sSeR`*s33`qgj4oU` zJ`GrdTI_TkF%CAoYNrL(c1mt7l6!;skgrR06ZcX7udMvTXBk+yS$Gz6EtFIo-YLVj zPuowurdT7pR#%o)|3_PuocIq_g0)ycN_J2$&tt>;K3(nTb0>?-VY??#kg(XgS6gKg z+@bKjOj<^TKE)L81{d{jJ)0vVhAgze8ZO>XkjTKLCm^%1n6k2gF?c}pO+*PE9mMcu zyB`#3+*3sQ@JV0uNZ;>mcc#U|C12}1_fX^R&-t_ZNGKw>8(B0rG-S`SGwWT}r&H^s zOE-P=^Ym4pfjnq<1 z>(y(QNn;N?HS1%PbC;~Hp)^p;RmPw+Xx$pxN*V{99-o4PN>Amag8h$3*1JLvRRySy zw6zjEAm{j$k;lQchNy_py!?pJK;+2d6q!kVd%=e^s=mtggPs}^o_#3?J>G!uLz1); zWNXu#kh3}(ILIDpdsNQvy~g%u<*Mz5n28$gJKlDtB~MYTd+wT4CADw`o%k+qeXq^H zpj1-U33_ZxVFwBvg!SM1hN@sWjXqjS<=mW;saZM9cH#pKI(9ONxupu^ppply2i_@F zpN{V+=jTY>Pk@8+kv!~*1UR~CNelFUga@XNDq-Y+getLq3Xf5QMf-L8*4nQBU`#r# z)N%bNp~#u%_Q2#ACuP+au5Av&>*K@7=kr4mn;7kJ(%mVKISl1!OiE3lHZN(*6rX8XM5DUt(c_Su zp!pM3?sM;))3w2)Eon`T^SL;1_S4%@H>;c0*D0F@(y8bhQ^%5kpH2KtqzOZg4b~#S zDb}R6PV4S@BV~$h(P`Ygi89s3D%D1ldH`-fk-w2DG)X@dN_e8f{tWkhj-_pdV+s)* zGm~+eh2GQWV$g>$?nLll@1djo>eQO#rZ{Sr4+&CCfr?4Q?rWSFi8T4X6c}o7s}lE2 zjL2Y4@=O9pdcv8w_pIG`2GMe{AxPlQBjfHmXst{PbVa{wUM!8-TKhpp{zCfuT?=x5u)35h-f=wPkt z8$6bnO5;;&6UXJEP`JLjYaf%ARvGH_Vu8czQ&^VrrwOfAy8)%D^ityEj#?*yJz`;F)`tFbqfI zZ-v?e7z^6{<@v&0X9WW`%Xu9=&%WjD$UaZr@e4tX3m^?opd65q+)4qHHa##$?MQgZj`dx>bF*|NjKkZJw0sc3wAvgw=o@_eh5}Qxdpv* z>itUwueHKzo1t_@;6j86`K;hUa}Q-SG#%z9OS2O}=pM=M-7x$=w6+Q^zjB9E*tbFk zo`d(V&)#B5$4%MI4F#QJ*R(YeIsg4wT?*lJBRC~LlDa3yH&j56j12gqXob|hDWMbW zSA9&!cQOI##ygzAeCTORVXe>4tj+JdwW8{SDj<=O=`_Yd`{25@@BcIC9}*QP(&_FR z5#&mzf~|_6i`Wx&$81T3#lw41FTO#@=;DNkG{Eguq2&J$d}3gf0;rXtV;CV102@Fs zBoOI_?D@x22^&(_aQ=4XW*9>)O!*#f&mj4KQjzTv(!32O2`n{BsG%9bTJ(ik^QJ4x zlJzrr#@6%2U2K!X*eLn+erdka@&_WlEiGQBY`^&Te!8%Du{HZ24SV^J*CuQK%Z*Dj zmnSH#_@WT(e;*ehFo)-gWOVKX6rkhakA+u+{LX~N_f`!|73b#ux(gXFrks@xhABKz zv}Pm2S2yE*!#QWj+?AN0Ta%4sgrwg*&hCYWP(A&R(cV#{hk`RK*r;6Kl|?888e~w_ zgptU%&6HR(JPTy1{wwa{ze_T&gJi(); zG%^>@Z;AN1@=#QFQciJ={KucgwtARnFrii@WIXo1;_z0S7=G_vPxtfDgo_RQl*NMbvJd2|Ey^KV{YE#QIPyc zFB1+dDa*@&ZL28Cj~;-TxE@^tg*)eAvORqp-q%r ziw-yP8+zCtBGTDjmhS!XcdbePBcpvyegXaLd?-lTyZPfM>V$|678-ly@w?E zENwj`57~O>q2{(jj_Nve{MyB`BL-Lx<)&JX)+=bvicZ3&QRD+NQP-23d+jC6;aMdY zr?V_}V3rTrMdSbzq>|B2x0A4~qY<#KpW6mM?=xGM-GI|ijxBa>!Hbk*MQCJF?3Eiy zxsj9`Nx6}f8|f6?NY*ynOMV(H=JRpT`!ov1k;R76{rC$`tMJUCy-bgLuY*d z&Y?|-Fh_eJ54MQ}70DeT$$f$wtoKI{6I{X{_g?#hueEi81HPsEuf=Y;UaHOMQNGmR zn(MBhw+9CDfx2QJ>4=>LahMFES?1v|OKp=09?2NjXrcsHKXH6yK2uukhvVDD6q?p- zLN~KThb&(%Jn_5YXgnrp->=DMp+FVL5GYIAwC`I|^i(_b?)q-Dm=_xDEK0P8o?r=^3Qv7X|{Su$i%F1wd66s`YBb8@Ojb}{kM2N6EKOQF4Srt2_c$1Tw zM-rj>nKT{be74P1>LQs7v{n2aviMq8S$v0!RMmhUuR{Xpkg_7cvzat1ngA!&ku-6K zpM)mRJdwXn6Pmh|y!~14E!|XAU4)`I9{96SmPuq5u@H)KYm8GI#L=w1)acO9jGbGy zu;g|}+(q3^!_oRKJ8kM-!>%@SUyMNkIb~J1YWh%{vpLIs5r#v&jETb9SZvT8pdgxH zh9|D?f7`BR-@=wNb?grSES|bnULy*}%>62TW_sjWu{=^Y6cFd|fH>qwtO0Ryet#%P z93cNyfab^pG*5%XX)Zct=3YIU{8Zc}d?oqSRioWf3U4-V`IdAB<(_-`woOK6u5KrB zOf{#fiF>eqRN|g0zyIvp7Fw!pq3R=XOogVb+BC{$Nxr!`8|<2U)hRh_n)_Fe9bVU>#G zs?HdyGsb}_Y>z5oj=NGN%vHCSBl0X2Z5?ffB7J~|5!&J8EWGPsR{pbkpI{#VQy2DjN7&#@rPL;Y5h{5R+}=PgJ1nTE`3bGqfV zG1DaYAa8{Lc1dm9J%~-WRzP?XX~FbDaW}(MYXJh{Wy2x7<&d2w>+l^X>vSs5WfdWN zixEP^(g(MjdpZUz#sxq?faht1Pd#;&Q?CmR2-ePU3RK z*X9eDa<+6JMKL}SS(lDayl+9P^gbOw`x@QcgoF0WSmH9|(2-XGn^-Yv!_th=XMa45 z21L(o7qG9$=Ye!_X(Ep!)7 zDzjY*-Sr)*c6-kugm=unFS;0Gk7jmGjxn@R^FXCtZEIe0$eUYDXFG8j5`DiXYzOg5 zp+w24%E459%l616K%remau>=|ow^Y_{A~n(#fRn3#tIEO>}2U{LpTVuPMhLl)q%f1oL$=QT|h6A?8Rz>(4+ zo~64Eqnr7o^rU`y|6x!MYR26F!MvPzRBH9D>00S*yx~4?LEU|C2Fhe zybOrh64h`i_?z}QVqfjN3il^TB(D8=6bH$ORBF;GzyrhYmkW9LI}%=B#ll8CtJ03Q z#Ng6B);4KT-q5o_y%o={dw8yt;OrFGQ_Dkz2S;T%pJaw}c}GwBUVT%CcaUqIy=LjV&0~EBTG^?4nAFL`Q+@J#Q+UcW@;K=e&*cHd zO9?m4s14aU#=;;Vk%_iVAbPXVBFPG9=uVm>!?^Snb`oLPn z^Pb%;` z@0=5d6CVT;#y{NUEL_k>Y5^iqSOu58&9blRds_rN>bm* z^bD_6msqcCC&oyrPbbe65xXkdd5A?jRrlQPUK!c(q8!`pL4=l`(>4R75M`fErAWh8 zYj)PHp3iBQp+r6YsfC!xi`n!cOm{&pzU!) z<7S9E7)dF2=QLBq;9c_NAXt&%?OepXj}%Ymd1sE{ScuH6hulu0MLY;JdZckMFKyT% zklR@y6;_~903K<2S8x>XNo zV(Ud{QFNhcV(Z77i_;Y*tr)su=sSlP9y~)Axq6D99}GWFyFf~QF5C+lQ_leao<(pn zhr-o*m8o-ND;z0Px7eBGxJXRzz}X)t=JnQ^*@ohfCsfrBn;$U8qow6|wq`IxypRPg z@^xNzv^AP=?5ROvwVE1Cmhe=T##3S6l@sg$n0-CemE)2CNFpw;LR~7U_DBQkjE)-lu3%J4`vDbGE#9R|SO!Q05n=XnZQc!#eNyLu^~RK*VW zt~SjnoHmq9CS&OZwo$iZw56d&BrOF*o>yt}**G2yNd#>__1Tt0Q?+7!84!Xu)SN9>pKWUOV z(+pCBmHSY+4^KT=CG#4dJ2FEB87uN_U3`3<+Hzz(xepD?Q`DxYO;KA$ZA>>F;jOQe zSh`|_pVQ~l`x1-?(Ij{uB#Cf>3!VLaG>wv(KM3BC;o7v?=;?9Y!b22mZ5suTTf%gc ztiH`DtA5k}TO z2TqU1AETOcYY;g-1QFz>iqDo;??9LBvCFah+MZlBl21`ya6KP;?XbZ(@Z*|4e|z{q zuc!fsyjJDm6Bs2`j^?yIe9W*igUBEQ$c90r<9#iSR47?9itA^zmSb@Zi)($;pjbO# zB??m_C+Vp)(GPSrBn`ZwagZjU*SdjJ`&t-{-8sxd>r|oigY7kYuu#@>NwbGF>sa^# z|I>UWWVUp?k|>FRAZg=BMp@(UFp^Y9MQ`3M;q1CJvfz)Y8NiV$v}Puc>SD1Wes)=R z=ZXIv9Q%fb%-cI#C7TKiw!B8^cpRJcZ5G+o<)vCWzS_3Ak;)bO<|Pi(J;Ihl!0P2TDj?HKI^gF~ znA8+fd3dJ$Q6#wL(j8!8hq$@y7(*y=WiDG9w!TX8wXv2v)`Dj_bH})hNAj?l7i=`vfpPE@i1)k7Rf${ z2853%xg6N+kXY+;a(*b1QzUoVB!|8hlH}0$0+dExLE2P&G97x6J%60syWhnE(VGpB zL>M4?$^OXE&~#C9N;;lK^uMZo&rzBm?1-(8Ef}gfZny864lGssfu))!aGu)IIcm$b zb?w;8cnq^kYbQtIdLF6T1`wP!fxh*JE9cl($}uyUR#k99?)=3hnFoaX^{>ghpEiOa z%tX#qJeO;Zskx5E+~cj2)xC^#I;J*86OmX;=S>TyxAbwFNE!1v7Z%kj|E;4SF*D;{ zz1NNW%Ow7FovqUxx5YTI8N=k7$X>zvRUGViI9PqLy!HoQdtd0Tdl$8K%5UCWVfz)f zizR|`MA;Hyw&-bPqpj77_AtQvXMfx4d`YZLE4Pi=PLCL%7uW5dTxlq3EomrfXY*JT z-Lgo%?3m_z@4Qs3Ay_LqNJkJE|Q0 zE!U7(ald^FVo~3>xQ%R6j`}8^`-T?xhd8<>?VNrPN&%j+M^^m1Sj2I|jx$|vLX~fm zT2z*x$`br~>OjDiX@sm9cZc%&FFRxpF30v|7vbCOmRW)F%b!|-@wX>Lw*A^tpSIeb zJRtvI54m$%_PEP)C0p*TI!V=oFa^thBUt#AuoagdC#S37!1a5K7G~sYdhwHa?^TOA z`T9eqN96I+zD>T;K3wgLS#WRi6^73P|I;%zDO;7VJEXo3gE>tRGH2zS6+K#EF`|*Y z#%+x&?JEhUNfhrx@A`u0beTRUtlQrNjlCm3Ce9ZQ9l1%YIdO9K9h#ETZ<@?BYXdV) zRY5p*{bsEi2h|LO@}2@>*Fic9eth*0?e{SXZ%0?NI7ku_9$Tqn&?o}(C^eYBk^p%? zNl5^)+EgugB>_&r!d*uK7|@Sjc1lg(lLXjKT&ZYZZHW+xn}?N7IE*CM#$Xf-zCQ3| z=hzIyGLH^QI;>cdZqQY;$@{&XS`~nH{%EVG8mMS#2l|pk(Lexh1 zrb`jImC4^ym@aSFB7cUsp$*F3wH3Fs&pw=O2=~Op`DW zB;)lxVF?lW<@Dzm?NGw{F}pw#svMKk#y}7?pnN{LLgbsb+5&ef{>WWYyyXrF?B9&mc zd?Oo}7icc>7bqL}IoiPbDjWFbMj62^j9JAQGrOb*DN}iXo+Vzx_Z5hF$75B&|ejH3E69`x-w{r9lb8Q=1x#4OyeJ4~iwJ;4YK!X$*bTmfnlJBu0nMIgIgIt2k7-i)U#e0r- zGy|0BQaT#{ol`VeHV344(f5{i)0?PKqsw=ZRHdKyna9OpeKN)67JL(1;1m%bPy+`U zSmJ|=lPnrSelYBF6n=lXxjvM`!cZl`VM-lR>X1^0RLhX1c@*kU4wG^i7Y#F& zGDNep6O$3z+cKL`#d8iKp5uu8@N>LLwS?3b`2jl~p`VO{VN$3jMF}K~Oc4u!4NbDH zl&?nlYLu@=dEhr!PHf{yl#>$1i*VV@RoaQdcbz!egt?@PkmE(XLoMOD)Kh@)YSvD+ zke+T7&CDFG%k*@@fT5A;(rgMcDCEEAo|wKtr8uLgaONVlA1*1wcFCb@pUUjD^848+ zO6V_MB-v!;h0Vb6``ok%5v2hZbmA34l|i~kmcLeZn!O}D&B>OzQQ-;P5yeGMBs*<8 zGguT%LCMHI8P@p}o?hL^t)xsERuxofl7v6qQk?ZEy~v@~LEQTzi02{QS#?&$A@Ydq zfL^Wz*cGfVTEcQiRutYtb)T(ICkjQaGn=2_U(KhH6w$%ghIYE&()|EHXY-gltPyV1 zH5))(@>!#<^)XE33Qg*ZfBzv02~i{`gI3RX%!EVoBJLqMs)Cm@*^0pJ){0=&(mIS*1T8D;NeX$_8i4P-koP)E zqG-TqS`L|K6bDmF;kg&yCjm^Mm|A5p!mbfBj)`gj*B+}cc(Xq`~4izdBw7O zA5QW8eUKzH4dRKC@-Gx%`Z)tX9$qeHV+<6Oj?1&@w!%aP@`p@<2|a312t^?^Lo+o? zvo%MfK!a+ep$bi_1n}vjQ?8wAlK8`NqYq$ak|eiNOu9+*cvZKLt0a$-JWBFBO-(3O zdPJ2Tc{<5s?N9R5yf;>XtmK}Hs(P04-l#g3Ctp>Ms3G^>5W*RsntV1@gaJSTgfKv` zMgR=>5`Y1mt`&emF@9}OE(T>7oJKVfBWWEK2USMRIZZo97)C-4k|jH!(d0Gr2&Cm0 z8o79d8%fyIC+-EoWNp#&u>urvDId*;h#Y|L>8F&w7t=bMjqiWCxuM6E{FWV&m+gt- zL6B~{B(5(c#?LS4VQszEji|iyIrgW?4I$Jv>uUPAmZp0v^Ro{x^ULL>o>;xoQ|`6h zd#NX)J`{R7+=raG0e3gi>6abPv`E~LGkw@ITT^CL@x$d7Nav)$d(q*!rm0KZ)J%(FR;024FLb0Tn`@pC92;D!gWmRF z*0dpAYVLmxM4eNuJe~V3(s$Lf|X|N5U}EC30C%92E>a{(^p_N znszZ%)32XaVwAL3UikAU4w6wcrb|IOy$QcxF67bgh|PZu#}wp7fy&a3w?zNx9x$eD z)}h87eTj+Fj8>GOzLN*-vHJ8+FN-IaZNl%!#Z@M_aKOhPnG-+B8zsOjd?RLXoA`kA z%5;iG|CMg_VD77fzswS@Ee;o#aBVF~9}<4Bifb#^R@-$YdY0k!IEsd2!hmF{aL@vC z2sPKpzo!|5bYd?~-M2Lu1rse*l(bnezoCg^I|2N(V5z6|H`rf5Oe9 zv9oj&G);>z83pk*iNmi!24jMvn#PW7M>BMit`t5(EbPR^YkA6`ga=!7HJHmq>OQr? z7V~w5s@T0~kDX^#tD-%sXwUQ4-OxEl>2AtEc?PX7A5m2k=$N7dib00A%Fb2Ef~TG= z$V}HoV-xuy??)m(*lXwLHsu%CrIKZyt#mc zo~*r@0?qABFVzpL;DVjk`XOJ7qXdrWtXFPPqU(t_Z!oS>M=z8&)Iv5Y7NSh69YF%WxzX_d2B01(NJ#4OsfZPm^v(cXWfs*bQ9)X&KZ zP3bPOZY+iSQMjMIM=kbWmA}tiQ`F^0vFXYSZ6`WuME*QzXf_JQO~b^)Xy5&a#!u+hoz9~NA~eZmlr?@jzV4_`F`-31MT^4soH5cxIX6(iN~xm9y-wc5 z(R7X?RO8^j#;~zxmhKR11odh%=EAkUMIubJNLW+&J(DiX2v|Kz{Jm=O_~r0hmQ7~i zuVS8N5(F2M`ZR98L-tsWQS7W9taJIpHI^x~I=XCG2K@xpA}XoR-#%JJHJ8*AjIB#e zJ*g|~w4@$vkG7(E%<5XI^(m?vXMnlhtNGj?j0$HPc6^AEluYsM)$~?ScM40+f4`P( z$qJm6aLYJPCEqBJpDMT5ox-_rfpVz(4k zC8w_Lq5-7K$OO|6dpSlmX-y}=J6xhVul3;G7>AT+HVg5gzDjj!nS&_biB4_T_A-dQ zW|}6cNHlT#?8HsVD}zJAG1^N3)sNXlB_P#{`?RBe+;q;YbHg<0m7DIl!QFETrsVJB zDBq+bH|!rtbS~+XXveK+PSKpAxmKEE^k*=BZp*Ls<`h_}zA4bXiUyV3JA@qF!}p*XtA%K8#=*YJX?OH_f2WD)ZraoPn#?GsC3ekr%i-w&n4o_N@)$GDcT@{Hn}UQ zPwX+cpvsg+NyLe-f`sWkS7kF2FUWo)!Y#`1Qhd-L*Spedm0o-5G8HXKL1=j20nq`w zkvVVxk1-X|DWW@hqU#fq@tl!l#5)9|trzbEa3l%oNfFs63Y0<=FHmet@)ZZ;7)ED^ zV>0<_rMj?u%f+lK#ViTwM~K;nSk5FINHFKS;AUR)gVPq^Nir^w0|p$OH~^rBd?omT z3mhMie4SuiI@!tT-_ydl(vc4y>|6A5-U&LVvqZ=fd6-2=5Q6k7HB6~tr>=$>jODHk z@a(!vpOeW1#!c@VsbfWimlN&TiuM%kDcV~?dqPS-Ez6k|4Jdo1pdxl+jRsp+6+4e4c69uq>u(PIuJSpW$(cUuJV~7z!NrFH$A z*1g)iq8$Y?ZE3#Myl%8)NvqDLmX@z(x_Y)?Phf$rr^PeYTJW6fwrD-$Dz#pz^{U{x zDtJB}kmEwB^&yFzlGg_1{>f`jMhx*?yb#Y$$l-H0lt-$9=T(L2G8jZL`E$MnJsE8% z%cvpk#8zfy@~H^+lgE?9GI^Nj{b}_0a__jENppM6j>T)vU#UtwId0Ybtvi2t6pVbE zYWBL#UfOlf#w@0j=P=EnD5C7&*3cqO5p$Qtn7iD;0~x93iWja!%Y{?OQq#A-C@DFD zn3}(}nPK7YK{-<)OEPY#xL^DHnd$stp3?ct+ToI4hoM!jn!fhw%cz&R+YHdj7;8zlMuKUMy$&K>%sz> z-hLuVzL^F|@{TC%Yd9u!62wJq=UaL*5BAdP20a!h*7uaovWgoKtE4zdJiE%i(rIER zp>%^rV(+j7h$sbF))vp+o_!4h)I)Kc(m-3dSX!6oqsV0VbQ}SjGU69(qesHib}Crl zpI(adnunQTU0j9JmVt~l21*3CZyk%foyP)1ER_P?A!1!~S8yMxvFif;r4t53g9AZz zBHcxwb_l3t(l>7|{@j|0A6IEG=6L40jf_m93iI66&p9$l@UFWqz2un#%V3#nIX!u) zDv`k{{zEuS6$5(2gnH?O8if~>IhauL;^7S(??kMzjTWTAN+rAtWV-(B zr?GH|GMm7w6}ndIa1f0blj*}Ll-o1Mlu^QFpj$f4qfk?hhm@W5mf(jtn*aaoz1?!# z$d)GhDvFBeI5(gQnfPz@oCw)fZdZ(J+iknr5p&~!NJ!!?lF)!;$*RkVI8StL=Wb5F z*I8fU9|A~_B1HmNwYOoK0FsHsw{rcj6(~*R;>M36TDIp)n4CzBQ%1oyYFY+GQN!qB zQoL_p77>E}E{mulrBzbr8#<6}BuFN}Ql2De znif_oF79FA=pG}|DqX`uI;3U0w1!UXXX8+{mMo=^vSShjs|93PaSuq^>5Aq78^Ums z%pLOiYPQjr&GAUllNnJL)EAQ$PhoOk&W{5yX|tfTSmZuZKpZxg-bvavi%JXGvmkJ@ zu)Tf#QbF4+Xe%bkH|zo4Z5uR;iVE=$GCB-=6WnqAihaY-Q#Fm@;PuIlz-bwoA!1MOXi&aowkgkNaW96j3dA175i{r(TBWEG*q0M9qYL=2Au$}{#es)~HOq)qKNzYc#JZAiZmFX_< ztrVjldYVxTx5)9z16Y|ZgM2zHCwr`t)q6k$;`8NGUuU9Wp$j5WG3hRv2e?XGg+bn!&7r36H3-uPOJ)OMzkm>m1H zyFmf&_ar(UZh6Gcu|Z)6^yMXaOj3sj5K`=tHIM!~E;q?*^>}e<1$I&Gz)teYJdUlF zj`>O0-a%>?vSz_i2IpVit!S&As~wi(YL=>I#GMzo^Fo_MW5B&B)tI6aDtV=KS*7|b zWs7EMW7n&dKfccsu)3B;X1ySd?3Vz~{w-3~S2riIKE@YuiPp|%{C2qWf}PoPNFdkA zB&NbVHRKU+Rr}pMS59W&LB6Ic*flW6>26|?(^$*iGbseZ*116ySF-0y_ADl`nADHL zD6cFgHFBeSe+9WaW(NxLAN&PDYVX{G#&g2EXNb+quQ@N)AOPDp{AN_hC z1$x;E;cQ}w zrAv-c78C2kwYi2{n=7fn*RZ&E4nFjLmtuw7XOY0? zZY;&xEb{p`$W@3bE)PYsQTP}MM&9XygGeWe|9{%H0|zuUx?8Fp<1Kx_3%U5@Ph#rG zYTUznn!K*dDDWpwt8hZlqeoEum0;-4rhg~Pnmz#CldOdB=uIYn)Jou7UPJGI;WsbP zG@R#QxG5eDMr^u);G1;Cb1hO~i}i*>;P%%7Uwr%E_hnc1!7WvhY8bqW^}M z(%Duo1_`9)c}SKs4OWZU3o>}M*e2cAbc`$fZAj9CNjJvd@QE0@c}1Z#>TffS&P z8j+7g{q5Q6YZNh8myM*Ad?VfD5;8)?rwmWV-&j(QKiE2bFzomPT`%B!N&m(c_Vd0T zPd7_8c5L#!IHU!!!;>AJgSLR#M68O`718Pmsp~}IdQRf{m0m;PI#D>$X-wfD+MLw& zgwz#<6RAtOA*OJAV}a(e9pD2wSA{KX^*8=)9=`jLTWA}tD%vA_e_$c}FUpj6^V(gs zW2*JzO!@Z0xSKisNsSpn5+9f}{-X8?u=A#dOP!l1?3~o?=4FP_^<5nXmeyUx-@kJwU2A{v56!&*7J@$%}H3!cuObE?3%% zXJA`jYePh4u(8lO%eL4M!G?&z8zL+VW&(r!^ARLMDzZjhB;^zyRO(vid-;i=~$ zWUVWKdU;)GgADab!x~yE)W%JIP7a{;oPus>_Q7Pi3WC|#zahU+>ZSKG)zmf=gETc& zMaJuGYFb;j{5Ty`n@s|O)O)IE-1v7%OBPdajOQ^SBz`W$Qt@?xV0`6qTLQZVT>x*p! z5vw;4R`s&#gxD3bBL0ajYvAQ>-_kr56QuPPS1x?O+I_!|#@Xq30dr#l38x?2MW&&>ep|UoQl`3P;QR|Oa(sE(I|4aH_PYjDQ8ls(DY^&hLc&i zTxP^t=$eY&Lz1{H#W>KMhb~&)2XhUgIZai22v)bWaN;O|pY~=EGOMgNbRXy*DJ>3O zcCwX9LN_6tGg>%-dFcLp!*R)HPX;r?7FQ^O^8y0PEIYzN;WknpnZlUDhE8F^vSmzS zL>5*9ja3ERH{p+9Su5h+;7sDHb@V(^#nv4X*&H&Y3Rgp?hkoc+@TMNXU*_n!ElGNa z>7mdH-ko@N8v5=ul7b@agk#bEG?E6z>QT_{i=+UvT;!g~j2+YGpfaAA%uEMGfk0#? z1-iaa(9X@$M66P?P8*oc#TnQva~3ovY}-qSjOfB?ptASt*({->LZo9BhG0=F2gP*! zbn)mDnUJyQQc6qL!PZbO|1KiK+GH80F)D%ea~RJ5p@=r3PaxmRU~vZ%B>j_SKfjq0 zC98C3RJ4pE{M4VHEc44IwW5?0te(uItC6j0n0LJEB5k*n?Gl^s=y{hLT9J_$cyRuwC|p63p7rz8|QBbwoE^*q| zC&691fT9$WWfIj3#1A7>rm%vh;a#G6D9KxIN&LD`=4TNRl&D?am3?6Cpd$l${UCe^Ng1D-CUVP;1{d8F@S~B;4#FtpH>Q(rXM7^vfkq3d8=}&&m9fEhz)S)*^3tOp1fZs5GUVC`a|d)hG^n z@B&A=N{17kw}35wwgZ7)WV2Mvn;zB=A|&C*{)GZby;7lI9u9xGl}#59E|U=Q-1R! zG%@*&o@VURgg#(bRtw#91^QQ{3@Q7Pf_~E87zS+}g)?8Kf74tL-=^#c@55;NH)fHA zk<(&-T(3T@Rkz;wKfqlr&{Nz!817H)NPe74Se_OkGLM))Sp#O zCrC@ayq^4XyHiX<~hO$aAtP5Dn3ZoygT}fA4N0r)JJmB4`))sZO$qb{Z%?8Of6Fy=|I_3t@2c#uUBdSWhEf|y4BpX|&F@pnu@AK!e}l6XTxLvQ$QL>}8@ovh zr{gJoxep)V%>_~25eS~r6ThmwTAcc#Kvw?#S4iR!k~#i!8a$TIVPAMhuNyD4^$N#M zl~(!)U3zItU0~~CeWXjwc_&4|YhNi@g&BOG_$wcyHT_(uD4_PT{hI)`5gZX~)F3z} zNt|`39JyI;6o(TWkI~z+w~}KgMY4te7oliArIkztvb@9_xgIsLq|@INx+9AsS$}Qn zjz>Jim4>yGyhU@I`_WTjHMP>pvH&(H0qpDrVKI#N@P)#XBD&z>AHjMR`Lj2)(bEg~ z5O2oxD9BuCC4{z@LC;P0{6btZg3J1AE>*M$-=6;KfBk>|&-m|PF%5_x`hOCW@imS3 z5eXXfBQ))NqdBM=wZ~{>*G|JlM)O5+Yn|_C3rDn5@1)8D(yNn( zgx7k~W7?x=Q~aL4#SIE!_1dTH*FGdp<~HMI*FL?XQo6~T;qI@MvNwZu(#^r9?W$j; zT56VFk{4g&LZNNDFgPMhHHoV?aTA8!3b{KX&u&0DC$7D=Q!#Q!1FeH6a zpHj(h=w)mFxn3wPeiQyk77v^@+54nHzEaJh)sx0gL(i)zt<(FF5VNr@WVbN`u zu((YtI*)`!?}Q19;>Zb$&q@yZ<3BIH(X21t(<&Ap>D{~U`2IV-j{%WoF$yWI*t8%i zGFGCX!ts^iXYZZFF|DW}pQJWQ)5KK?ok}hgF1gRHK>535MUBk7PYN~pHqo9-TJ|e7YCW0UzaI@Jjj0`^C};gu zQYv;dSM0E6`xEkOunhN*%jTrZmMa^cY6^@{Z(XXT=*PP4y7PrUE{rBtn|jZUynu*{ zHl>I>AeLd-0~Sy&MANFJ;t4!nDb_%tD9}Uy_XfRcvT4;){)$9|bkzuEBX9zAkb146 zVCFfJXd7wR$dm|(S?R*Po;d8p!rl47u9Y&IZFQMW0l`iQcd8niKA6CamswJbOt*j- zhd7s>SyGO&e3^KXDHk?z3UmWk1{KC$8`6cxE%7Wc>Fyq3YGxXRNlHE_ci;i0E%~mQ zw@rdaYh>hIlakQBAFmnO_X<`i8?HHsvB&DR>X^AV!zEdU>@36TwOU=gul{sK(Q}K7 zx8=2?kV5hcFUIMzH7H!<8nckVN;+_$v<8V5=Thp0&mR=Y0qk zcPg$2oF@J%4zAw%1TAXhUtCc^#&xxsP4X>>iYzY*bNYDN)qOj!qE#+q(2Epmo8CWvQ4$@D9Q>vw9UT;Y!ZyawYdT9A<(fl4m?7 zbFDbKrCIccHJ_967?tXibD1RbN10$rvkq4H*|>Vr=U^O=6+Zv@?EEj&+uH!ePZ};{ zm55a$R*4S5d>VFeqkR{St=wQs24!w5TFti$y()jN2V{%>Y$)n+T%$*n?M3k7C4 zPMq3zqMe-l!h+n&f=pbEe#q)I$0&Y-1v<($&W+OBdltXZOMv^H07^i$zx;-1IhQRo zMGTZy^^nYM5vuoQ8_t#K=~s?)uS}#p{JcMbWsy&|Ej4u2W&&dZI}6wSNeK*u?FaVVpQ!~)9KwEejN0Yw(PxkT09w`Vpw1w%qMh^mC5nzMPG#7tN@`hL zFo}Meg}7i=g;*6DxTKcIXIG9eNF~aQ5_=I?7C}z#M$NS84-*&@*uV)avo#!mzzXTj zWXTgA6ByeX25)PSx-_mvrgbHCLtc6I6)M48xo-ToLHrFoF8$j?Cf^H4FmI*dS(+(W zh-4CE63p%C4w?Lj%U!;V$eg_4iqK4C6S5%5g5~FA@-T4ap%9RB020eWaV*2uEESH!L8Tm2I&2cl zJ*kZZ_EOaWWtP&W1yCABP#!#~Rk}U&vICOSR9C*2J&<;2ie(oHw|TT5k31W7Q-{zM ztKXRTcmE-I!?gZ+_k%26>|Q-M?MLdtlP?n_m`Lw7q-KzX!Ps6J@J(%Lz?&3dd5an0 zNK>;6;`p9Pz;L$pt)Sl*s`-aF^KyXOx>hs?E7OYJ%!t>diHfCF zZAEF~O|W_lf(0@#!{zGtaI&6ZPVv6@CR{DQ+=uJgO+BQ#Z?3@}`;b}Tkxu=jEhE%xtN59@j!SX(w(a)eh_Pgni zUy?SLAHv5kXmc4ixGbD_KfSw8&Y)HF;o5@7GwMu@wdZ|4x!dzjG1)7%xip7tQv~#K z<*$PJU}{I-Aut=mwCQBrE+?nT=JGyZsuxZjmorY3W_tX3w`~(mnaeSkV=l*BZfKcD zFXD0%6`9Y3%jLd$TW{cUWP4*S$GRNra;(d-F2`K1HJ3vbp3`u-iaQrdWn!^3C?07f zEQF=8Lzf-8!*=MRRqfSC=mteiB%h^zH6mL8z7Ue4DRzpYcqAH*CS;u)lkA<+Ufzn? zu*p{fy0bOOmXl?$SccIl>Y9ARYx*U5O*7h?s-@Mc5~0i#suB1xaac`ja^5KjquM#Y%K!jF(jst z8if?G0LVeJLl2rA85A64T?wHp3DA=CQR0JsB@DY>`pao}_n@6PUZgz{Q~V1Jw4N^S zgXo4FXnzK2u{@^`u_GuL%rrHM3==xrDvN7|5CE{g!%3D~8cv?FnPNg^sjoGqvqFc( zos^jk*vkW;EW=^wWzJxRUL*7z#NiwUeT7ChE=uI^zQ^F(yzemk-G_;Kut^{dnsu9;*B3g!K@T2cvz$CCwjE?Y)OM(s3>fxZioGm zy=gb^M&OyGOM-BjcK;jG?%^7WGwtp`yNwA~Z)+2-#wi)gLc*2pX(PimHA~oM?$!JCY?drcmH_LLRf{Zgnjv+Pv`IQqhJKpcrLLQ% zix9#a(hKjRaJ`r;V>`b3>Ca&}|A+dKh(6)9T>8 zP(M;Y4G&<|RaHs17vE^yix0u=s*;#Z-ynB1U0SQym&79Pi9rsOa`G+34XET>uU+yj zV3+=R-Q?S3B%y7& z)~%@M_{r9;xib}w|BQ*~y(q8SyLaF5{cL<=aAi@`?o5)2ZQHhO+r|k_Y-3{U#Gc^9 zHYXF?_QcLi?Bve-e&79dZ*^7ou2tQ8tyLTK)YH%2UjOi0u7-R3{D)F9>k$XNFF36j zNp689-nvRgTo}(w83a&FcnxV=p0dxS82CZMQ9^HWv}E^ifqFvzh#Sapl4-eDD&xQcY;Z+a*tp@}Ht zzt<~5_2FkD%n&UW1(#`q5$^T->Ut}8LKo9?xwT--a0Nrx7^5)f;t4@>cTbNtM{8r_ zkZ;09Z`Qm96?Kc~K}8jF>x}8=9WhLAP9i?GSm`Szd7oG%btjknG5IF+_WSc(g0KtW zYC;NLL}IMZGyUQpFRx|$BT4{96+MI-?x=$IZ>16BXio2k^wwe~#1%{Cbz2-*2-Q^aHUJ-Unuv*vSj!>~I>a0aPE({Ss$)~f(9X%nY zaaEc4+Ijp@3`)aEGA}pxsWY*^jl;=l_Ff^U!4KP+X5v|SSfx9E{_0d$u~o#d75{9! z^mbx!(R)u(NUXCl+2Atnwhl;ap`CD}iyIyn1RU8`Pg$JgRNWGm^UiP+S?jj=oY_04 zkhb>G)hFE6P&Wn2!2%PsoZ*6d^CE(~a|$N4L#{ z{cIwP^V_N1S51|3IL5Nc>>{j9L(wACVe1xQEJM*c8^^nULGK?49_G*(hU-Ye{9~f#l6p%;q8D)NC}fXGfeQ%g(Q6wIT%0llC&iy zDq>zGI;?eVgeNcV-3IjXw727ngt9}}^uABB667E7d422jxxZqOY_WP;Ex!aptmKcuB0ficEGP(CNh? zwR910gP%zUtORrxQ7@FLg_zo-cPXKr;Cc5)Zcw{ypPZ%kZbNcdJf%G)UV3U{PI8GJ z>6k5J<9~(>BzqY5GNgJ2sr!4PB8XbmP>*8B^OInuCxtK1P&5+qBk$kHsJa=^bx+Er z>gZQtduFhpjQ-5mYYxO?r;mA87*F6mLk@dZ6{q_+p3d9j z@EgikEcPsG=VKH3oO&d8uaU0ub*-hCay#LUtpiJR4YlyT!>|jt$hoG)^VNE~#Z*ZW z^Ov>f3lDKkQRKaf)LZP#C$-3xteRHQmAE^Qfm;xl`tNIQ^t|L-YLrXq=9V1=Dm>A@ ztDlU%3x2rerfbQD2Q!L^{h*+&{8@5%0e$r&H{5ICnob`hP|fL_XP#bGeVwv7uYAtkj*MXf+kz}MoDsXxoQ&aWt9p>pV1!2C9qM>=Tgz?n z=+~llV{Ai;?{DR^%-T_mJb!kjmxH9H_3w5E#{rF2hEMJ41YPBgUd0_aBO|a#_01fl#o13cQT7^g=SsFj_D&dD zv(2p;{*PNed8VTobDD>2FPUN8+kODu-Q?dHW_~C7x*6L>XD}s^sqwIDsl>2|Jf%6} ziPUO$;2CciN`IooQql?XOq05X2NIMRcN|3S$5a@ywaESa7bTdX$?5rut0f!I)7aAg zb=#*{ju?DaJDZi@4a!ha8c}Ua^CG=vyLzX7t6=YmL5-)W5pZw0UdjQaJRtdGt|$;kVVr4)g@zPrTjAMUz!U;o;V z2Zv(HyJw~+{7?~i$YLhofxk3`dQMP+!fSE~jVG|*{4HO$@ePsNvEHRv&7Gm7zW!vm zXu}xqPY=eHdGIAY_MRevIHAAx9gv31@koWbK9)gV!fCSEtTc69cD1AucFOtH;8(N9 zTq7XQm`UU!^ra7y`msAt>6ph7wM~3fFf;C*K!3WqF>lhmNv6M9s05z-U6qKXZ^ zPwzbgKP(|7GIRh?zl0lprn4wCJ~TAcj|yzPC;CSk*FD;Ljw)X&!#llrEqt3n-^X%nh?$MwkDxpZjKyXrdT$*po4uZ+^z_r0r~tvq%P?P3VV`NjA2T#c5`P z@&X%C=de2~28^}QixviogDcYtn0>a)~Ge5l41Wyyz2s- z#&MYwP%ntV@dP643R_Q+uLkcLGo>2h01SfK-NceGy|^=m zQDNxg&4`v6cdqNBMcQYBV#PnThK{R5^3b}mah%2*AzT*MF5O*HV&L@vJaXhEpUEJJ zT`yHACTkfk8C+;Nypr_Uf}SNpDgi=+x$D+){-u^Ti08^`$z@nmDJ*s)qplx_9RR0! zE(zOO0od^Co%cup7OdQ(8Rt8}jp5Je%_=|Oiuxn9$#9h=U#V4cGGxP#^tLAWY`!}g zfRGpk!?LIzBvB_cy`e_0t4A7oY9Ogr^G{(D4;cR{03Bcwq{Y1jJHc7b99=ic5_LMq zpu3ef$S2Xt*=YMMBn3QXyK zk}ck;Y8lgKOPGA7_$r$SPLG!fZXi#8R&_TlSqGsJjQ+Kl(CS`cMA+#0e4=hIV5T-% z=6PO`+F$F!JBs{%c;Qy`T70;aT^j`REpu_JeH;8jfA0SGABN4O8_%G0^N&yum_Yba zC>Zo@Ps>Oa!@S8=|V!Y-lDRoNvjZEl%BXySfDfA9kiz>7F(f3{6)m%P3 zph;{=$Yx)2f8Iwa#rD_`z6)PRBKwcYHMV?}h*9m^5O2AYnLeg%jC&;6_f|k=bsv36 zi8{+d_E*odX8FXP!JPj#%A7d#A$_JfvsaR8>1#tkl9$K9 zZU^S+Nqc>#>SPw@x1KLCoIl!G4Zj0~VE9zHzfQZ*8OA9-qbCCafnYULgrul1O=chu z8%F$KWNxHTgHv)~yUpbHQn%GBuPCGPIlEg}@(RBn-*UJN*r!h3HEhumC*+wJTB;Wt z)dgt5+D^8TW+zli4JEKVbS=~e{k|S6bmw{1SB;$$XIF5GM%JTeS2aNr<4FZxRl|W; z3Qq^)lsug7My~w*L|NyZjcuK^=SK1dkBnVuq6KyN+=V21&q2^Jmqe;lTcBwk-s89> zTPuX+1jLCn{nUQtq#p=jSd^VDC-PlxQtq?jN+!ef`}^ty68lG%X{Mq zP5%e=NzsxqlbE{CqQ7;yy|ae8zfLO(U17eK5pMQFAoJ7HKpEXB!T_Q&*ql{3yan0%?Jur46TktfFS1 z?xTf3C&XG-2bw6JGJUwS$qibo^rk8s#})hMC7W35XVltxCm02RvrOpRU)AMh+6b6i z;4}0y#Jwa>$CZy;IZMUXa{e-0KzzTQRHjoD`yyS$KEzFRlt4z&o-w-`jBq3uq3(+r zZ{^L0;s2Er6y`aX6g)cX?O@|i8Ee~aI45BAOXD?&i6yGDbYB#FxFc1vqQV@flC-r|@=X8<(~xhwe|5Cbg| zH7iSNFNDr+Mc%O0ij+kW)WKRO{~D--5gR$#*rK{MNr@y6+@c1mCJOt^V-0kSX*LZm zf-%$nhXWal5;n8`nHx5K_JJymN#Z8dn>7!?AO{<)iGON_Uy*ZnTo#-7j|cgMM2W|? zrJyPF?C{K_Yc^S}x$u8wDw%o=X>yS>3`=7Vn)&iuboKbxkQ<#%H9DF;t*?q_O=FF5%xdmoD~Gj1lR<7I--JLR zSR=Rcmj>7ab9|qg?S{Vt7$yir8nD$+aeTw0AiTe-#qokGe!7uG$f>y3e^J7(&3eNy zAh=?XJ_h{Luivi!`P5DSNofz=qhc-EL#tF<2c(hN$k@U;wlv}q*SW!ukS&;c#D7zC>s8}wQU^GG&FEOYk4r4f1 zZ@Co-U^8J!dUSgWZD{D2ZSDU=3o5>FU@a$WeS9=+C3j(05)I<(2traQ4k;Mc1Mh9`a$*5nV|Ya^^Rv?h%dehi|XBmZ1rv5 z%b$kMqqX*ff-~4{_t8~qu3);pwVg}Z^GUQ7LUyIoPcA|1Egxht(d0raI`G4%)SAJq zv0FhsQEBwrX#jRMFfiC*m6-ddNs!>AI3iK+=HbYa&@-y$q}yvz!liHU52SdXQ{7g3 zydZyGVc`S*q%wxVwJrLjOFPm0tut597BMqk8jj96vx}EYMzyQ)O*N`{h04^sz6*H0 zz`c7)$wR)ESLJErQAQ+$ti@B3E_Vt=?Xu8~z>UBcWa?IzmsvA7K-3@%xbHMD&&a8> z(d$r0x=Xqrei24++&GZQS8{F>tF+=PzM>+E;?|mUZ>4k^#w#>=BLF%733>O!aABGn zSe#ef5*K~qwN}2b)F)TZAQV4*t;Z_i#~Y&75fUHTFz~5Sr(&NYo+m09Zqnx9nWtPz z2PL^y(re$1^&ELjZ#Qo~hNlNK48xxbe*7>miXpcD^C~Z51Twtm^7mGV{gNolaGZf! zD;ZrDa?Y^TBmc9`Xz^#!t-{6finzzhdxWu^6N8ZFPS0z?pWgjf@6$$E@kWM}nc0+V zg`kZM2o@mO-lvz)*$tH)4Z?dot*u8Gs<%$rCyM1YzLI>O;Ew`|Lo?`3xs|F}&}x|3 zoElPL*cMCIyzrGyHK?>RgUs}NQSbNosII!OM$iEt?F$qT>D#cuX`T$jRX_dFPvA5Y ze}Ot+4qM9(QP5sPiQ6Z`bM1IaycHw-OWYo5?lB*e#OA5OHJ5fnEZ7XSP~9JC%%Z#R zQRrtWY1{@_lXx~}{4n?vkSN>i!`g9Ous*&^98g@Ki0mXs*I$gI)$~Kz18nYcODi3v(LCbA&D4Gm=`vEo$S;$XN}hObb(9R-G%oSjud`9{k=LQS zHL9Xza<5q;bXv`OIsiKpZ8(6VD61-{f)Yd5;?i%4S?;im)+=ZWa8rYdBh@^JMUt(# z1L@gsi-e>+k0$P%GP@O#c72;g57ypG1iKojZ+|Ch6$l%=YX~8C|8yC(YAm|epdOEr zXRq+L4p}JKF;e;Hpnfk`{qk3bvRN6UMr!xjxz>D7JUyuu1}WQ4_hF^y$lJHG>1A}5YS-q~JJj6JhXkGu`pj52VXsQ~8UJ$#P;M>U5wL0O@NOTU z&~^J52e&|~q2HEQU_A*?ypH&Rz;V#5t)S?Ul98R_aSKcH{vr>O717t+tc1iGjB2Z_mNeTJTv?hgyx;v?w zvhHZ*@8`w+J?(DSkeDF&T(v*;ROYx-7O2_!3VF9Iz^;xBMOf)nGeGdYYHShVYd9BL zMgj#P+`T6b%x8{s3;c0`#W6r&p{QAFawb|aHe70~2d!j@Mm72B1AE!jMaDl7iWfe^ zt_?dvS-}O{F8P^2u`#-DQSl=yPlp^5nos=r!#pIY%@8id-uc+Zxt5Zx$kPnF;x{Ut z-uUe_Fs@lr#-&T*Weu<^KCPd=Ep5f5Y$2$IU(A`pDgE;OPHMMOa5(RUBq`RmGk=9M zMY=9D^}>B)nNY)x2n5;jP2A%3gz}54-|I=<HA)-wijU$nmoLMP4f15U749mT% zda-BlLS43d$;^{X4v8yhngLfQ%3|Hfb!~erUbVzwFg(%F&4~$yjwxB>c4XhQ!~buF zLL*%>=8X`YqEB?{atecB0`1`u#o|`6lEsMXqrKWcA(TbTm;2(S-1MTFWYMI#{tNR# zfxWvFDt9DuR*D2)67*y)`NsdM4Or&W#wS_&$$!L0DUI-R_Y*HbH&DNY${^ICI`TlY zpI%C#zM@b!hG2T4Y;wCx7@k>G{l0wKSW%vrF8;E__-ma*>6Htjv$wL%=Q>d6nO{jB zb~h;M<9n4Z*uUZlb&uBeQxW4)KA4wYHw>0O3-bjU^D3&R#25JiW#Jh!L5LR~)MM@P z7*2cZDMnb8g#L74Shz;PUbxo;rIDl=Xd*4V)*+uV>P~)^cl-#qsl%_+p2!`-Lz$)p z+bTr{fZ?ct_+ETJLtzdMNQLtyZi!>}kqv3frqJ$Qwn$bF^lAg@`@R-xa`!({ssJU1 z#SUYJlM?wasjN^c+H{$KM(DO|GaTEYF)BJeHL5)2l{J`u91celXfl_4v|M4|LG03) zf|;Mvov3=0hD0^D z9*ge%fR;AZQPx=$val22(GmUAi_BJODD(#B;r=z}Fph(2*ai%ido___4M^s7vXRcOUltBypdFWsN}kwnH(7QWCgUG;cf7iTf zO-!nQ(e@x4;sw?~5AU>ADp{9>HsW33yY(k_a{P55LQY^D3qM)pM_a*yS9)OIJ**c9 zxEIi@=euf1P;Wa>Q8D>g9bxk3yEMaX4MDYFJHF3;gU!TMp|}yNbupw3e>sA5#4vQm zKdF;h$7;f-TC$G%6cf&Q!*RfXZ7)6Yn*l|P(4dUS-#EM5qK`@w#8= zK-PT3HQxNzB~W;rZAC7LxLr876v{`q) ztsPH7yC#}2eN3Lm=xZAlb8t;}>062=s)&#F$C!Im4{7B^Kw*pHG7=pVTsVC2Q=Kjj z{6}>&V=5<_==q{ncnpODy$Sx))e1=ilhxS_eHljiQbs)bQlr}5FRfx#4tXy$@Q`Kk zivjnU9`5$GWwGH3uXszDgO0sJwd;{qzVcQd++-19T)dO#dDap+s$_0s;zGljX2oUB zxn>?Pudk_)x&c#n|A5gqF1j8rP&bckCLY6%95~%Z!_jBzBV@g;iM#mSaCjMO@wrRExb# zlhTx!5rV~R#AQ0WCy>izDfgZ024})xF+9ZoAsI20mGZF2>P3nAc}zi$<%LKOTjal@ zKd{Y_?&>Db@q0C3T=+3UM50RvrgOkziQiuD;7httYKiYRxzzrA1Z%~%(>SDBUBBUQ zwI3gdoR`bnmz1To{t8=IhnPS9aSCc%9y+)zo*Lcl0{+E0=Kr?1J`r~1S{2E`T^LDO z@P_47t5%=Otn*<`$Z0UE;Zi2+g$rS$5HY5H#LOUMbq0M>N0cWx5{^Ex`g=Y~&&Y-X zbfuzarl%eS^=$^$I>p8bn}tuho^eHIn~Os^NX&}x>Un=R>(@uTsBh8BI7Ex3OnM+# zxcw#Vvh$2(qkH_k{X)XQ2gq-h)J1(mt88nsTirKxFk#@4d?2hG>?vpa}BJVz@EHR4&m0wb5NFYBLE z)7Ut%%)(f@k&YbC&3xwRW87kHoD71*+V?=2df{{#Z(Zq^QfpnXvi)~=H-qy~j2B?K zlVeprC~FHrX;-@a2>>61B0^$JJha1WWJGsZoT&wFs?Sjn^R|H@iofT*8P=JS~b|FWYsIKg*t z{<~Q=NCa7gMI|zzm6h2Wm%Kuh($7X5$sp_Ut>P=?gfPQTF-dI`)#8hWP5l!KMEdO) zjim`=(qk<|Hob{*G+@NZ{%AyI!Ri>Ry~L!ftVSEe8W0F z{wld`(sY4FV{=xiKz(JaD%lVG5iz$wEm8hrkj+xe4fQa%Nd4^;M9~*0MBUbxl2aeO zJjrnHrRdmD*M}{@(MAv?b(Fs7x^IbgivCW0ns^8-?`8k}-JOml{QL)>9Rh|*W3K+q zBX8zZ5#{|~cS{{x_|jEuJ(L10MK)ULF=nux{-L6h!8?cCQnMgRnwl*KS55n&^r-;G zE-q4ZrZyMaLKCv#a~P+~W+#BLN><%Y?R&+;?~1$@ zwxw<6{K`^whGQ+c!_>T01*H`vGQ)V}eCds-XLt`;O%sb+3D;7wMYtdppRO}fb%Lx- z!*IaWC+${#1hvRH*Uun6(71g;nnJQ)ngZW%C_m8X>0!e*xu>@)c^T?5*t$|4kUKqg zn)|{*7kJAmm*ZDKkif6gQD+Lh@5`9mv?ie^4DnKx*VHYT zHQ7MtoGjkePO{Ig1YA*G7Cz@N)~+Bco)^@D6YWu7+|zSOKX^S}(nHqI2G7048OC2+ z@yFhhZ8Z{Zk06mpu8-OkYkg(~nWkuoT*7KIL#q49@!8ZbUg>*StmBfk8*;rMo9b3# z<=0gH?H;SDpR#U@BvjsUs7nyvaiC8M84->)F*9K>Nx$c3@7dOWei*g;t#5z4r*!4I z4zpq|(C_QU)VhrzQWNh*b>ESia>kj*mJ{#QHHhy##Kpb6G*5`gn^(-o-0sOe+@)Ub ztxahWW^Rp%>9=QCuFl29++GUWNp|w>QGJhY@J?*5&bliC@08TW9Te14@BK8NTD};( zcQ<~0Cz+UEk(ipqmD}hhXLwkWB^MQZ|K44mOtutB)WmUiaj4e=*O=1OA!r9Vc;w2R zQDBA9WLzJe46{nI6BH{O+`U7dBs7eZr-`k=fa@&!&Ng{y)i!wuw%q`8T|&{dCj0~%U@CG7NXTHhdhR=IBjVw$8<`kGub$~^L=ueK4RpECKhWb|$HaK*!uN~BS zJo|+j8A_%TnZ})e7Zztfi?=-4f!`Y)Mxx5isvCfEjoL<(5`O$+{Jg$(=V2~6pBaIg zq_}O%o=?!V%s=XuzI9+Xd9yNofZ~y?;%k@;H;h=PlJyO>ei}z5D@UXZaVMFBg_WC| z@$FHyRrjy|mA>WskKNs%`U$;XE11JQiK4?Yex*dj%}CJVf6O;uOG#Na8*@M%W^t7A z)18BAEsZh7`UVU4ZmVRZ@ta&9>b+On zl78SK2=pDyD?~MEc1wq=Re6u9&O*gp7qC%pgJQJ(i@g5)+<*P^Bs?s+y`n~*IukSS z+-lclu>BIa&V1^gJ%5f|c?~fpphx%zQor*YAy0SS2onm+uMfQ}AOL+6Y?t|X#sPen zjtB=Wbw-em{XGQN$t2^!t=w1#xdV|RcJB%SQ%G-d~+P9RRdb3zx^D0>!(KZ{NC z4%F>;<74wTP6=*f_$|f+FG|LWls-c(0~v>c;2h^g`n;k8snDT*f%7qUb%xidar?k%?4ZMkx?6?o!6yOa;AQDg$Zaa3TO5BzL7hnr5{Dh2HNseL*Ez zfj3wh+5$K9i>Ky*zOqm3r0UHGH5amY+d?3Rv~KQz3J~-60LGIqVERrwk-u! z893Wj3H1AFZ1~dQV#Pk1YIsZb_)DtA35u{2D)NM=Q~U5+bSQC-3Q80?wg4O__}ii0 zoMXEdqi+iFmFY;u;?PdSOVx3oTynt zSh1}a9q?g!y4eY;d=DpzLm!;PbF%vK?5l4H^+&~&21+IqaV)b1J8WczCx`aeX>9lz3jv`MDq(`R#`lvmlwOD@E zA2&G!nqd{O=c3=$w_rz&+vSSMui8pzrXo#sU#}&tYQ3ycb4NXw%f#|nd`*!CE(&~p z#r)wUVR^mc%42fBqE7B9mVCx0sy|g%PR?kKcLtL{=RrwGgN-AzeB@paY9zZb$>UOMR%SRNO2cf2(vd7hGO59NyG( z?_WdhItUkMM??N+G@_%>XA_?5XuI>Q9geFKSn0x3=M;`>W>ikXLnghTyqyzokTX)l zUPZ0{pAx|BR`*XVb$iZ?-%!I-+*CvQ*&{WoMN3glsW8;|v#l-+sj%240?f*TexPmJ zO?a3=g9;L!LY^q2jYutYS(jDnk4Oy8>O5xVN$9C!?Wie7a_(GJ{rEN8Rrs1dGD8u2 z@`ey_3vR2pZAWM=zrCWZG&co`Ntgn0B8qLjk@lGw2LLYGbJ z^k3cYGM0vdHr?`Lo`ByyLK#coe)oM}vj&B8ORePzCOgXb^T@0Lb~x#pd<=BkTQ_aG zBA3-n|Fm~mBm@GzjYA3;$%T5j_tMJU(U7wx-%jtW`+e#MGNRWMe4OSTLC5frJZkj; zJG!|+XtxpV?C+0Fe-osJQYr8q_%ibwt@_bx^N|U(ZWs6*beIag=PT9K?DjXx>z4&r z3;7+*7t>C;9UM3r1#{|jO2f%(X!UK(liS(2g-bg(r44||!+*PAe{7z6xfC}l?@4sK zIj#@J%#;PTMRVP%9HA9Ni%W^QavL|j6uk&&C^4QEY1|tt-;IFLlI))pY2~?8sl*+h9lvz=`?YCq3 zPTJ{d4i8D|ucv*ZtZwac#lZjTmRX}{ZTH(N+uERXFB80kk{i{p8fCU=w_LbYbSlw7 zyIWR6!l@=OHlB@|0|)nW4HMYfyt~`)ZR$McZR!O>U`@QLO~k7v2$pRl!f?!J<)BWH zTlhf0iNs$E>yc~#Ug%9x2rMNNB&X9BsH=tH;}I_$B{NWFto-@+JlPgtPo6b!`R$u) z+8|HIyaZ$1Yv@ORdWH4oj4&+_eyuf2-si9d2uN*8cMDM@MsOv}3~CH(RF*pCbU$Ru z_#8kB;}k^8DA!j5YLO&#mcFp&Migqia5NB^pD6jd38t|hWk)L0sfzA}Kz@suK$0+6 zD$K#51d9>*ZE(xj3Z~tJaM}}GX8|PSje3uin<9|f%uW^ws~FpvB5G}-NgE=PF{Wka zrpNMoeK2bFAX_sdl6&S$SqdQQfD1ZrMQ;p6_WVqKRis@@Gi1@Q^km?#w~}X9zMIT znNULm*P9jo2<1jntj7D2v?@)o;&VH}^|k#beI!f75iO+d+Nd0vV?2gjan|>aC!v^^ z#}kKu#hX<8#~+mQbbb#=b=PZkn&J2Ges;rl=(xGOh<7hCnBan{C*|x^7j_`cbzf4# zVizaDFw3h8gT9YO6k~kB1+_A|<~(bs8h+0n2uDc_iqoHbfU1bl&snw{u6@o2Ir?v?5~21yiu&B=w@YDbbsz4Y*N&a-PrNyXhC1`e zLDjXr`ysshNzl`Nw(=4g_&sFtn?%_l29+G*Dii+UK)S}RlApQkXz#0QWm>aTof;q= z3-iy_FCCR(bE$e5Ch!rtP%-udtG2}LY$l$B%GO@zQqTtdg`YUPxY4l_GM``RJkDw1 zwI2gHeSU2=U!-$_)8nA=FFTgQZx z9Zs(QBi9|%Ds!$$>}DSB>Oc;!T0P^jkLP1LMUX8bdqL&Rp)sP~e6u#fFbtx2U|*0e ze{Y?26`j1jIn2m6(n5fGIhATV1;bU!-rIn?u#`DNxZ*+!_h})1K({-c6|5`&uZH$l z@dN)Z*z^f;r2^~Y7_I=2I8LlUKVvyVFH14_!f;nuEigAO~oesn1M4|wW2~w$) zwU>pdGn~_fs8;?R?IWSTXHav-OB1E|)DY8{*a?2}UfNLqT6R*Q0F}GfQ&X8Zf-zaC zPuGzYD3?|M1(5=WPY~90rgUr1GD*Eo8zHoBH55$2+NzlCR*7QZw@-{hL}j3_&(m{g zAm?OYPE);Xh*PtpP=zh^zYKGUnN2Q#QtH8TGRT#&8f}Agt8dm`Mx&(&nmHFs`^xlY z4f`4~5iPt4Z}+q9P^V?$ApWna|6bB1d)rd{;R{(;`kLaZmWE&6@tdthY*wm;1FoY+ zS3XWN7efnff}UsPbO&{%pHHjRxJJ6{9`gV#P7HHWfj9)U7oMa3v;vz)>H_nt=l@y7Hnw?YVQJUQ`D^k4EbKK5%1B;36pdQd*9aw{y3>pbR#({y?$ru4jQ1z%a}O zAOYND&{E-_afVgrt)-{nyMy zW~7r%v}&2PoM>j_=-(7D8P&Q2RmHO2A0&#LbH06NM7)2lVElM_uCRs7@2`jv9Y!C5 zjwX-Y@<3euwkF$$K7|PdE-0rR#wf0E{M+?H39hPEL z%>L{1Et#8{o6yRRwe=RRu_A<^%|=x86-uuh_qB$p18#5Erm3*aMVO?G;O>plQ&}Y{ zOc`>bj1B^~wqQ3N6`!`~%{>&R(VF$s%)>=i~Q%P?Wh}wv>2`&!Fsz8jmjuX1cnPP`iDm1|9V!kF&$xVS+7VMq4`De{48vPp-CmhN*G@i=+)aZ?RbvG{C1)?|00u^)EroQij7^>uuQn zd=Vio_;{=Pym!B5jekpfnE88TBC@SK5|-rbPZAzmCZWy-2d_9JRA^q8krjTMEUnRFK>3U!+s1hI?S!SZ@P;mEO5_s z*L454jk|N54MT>zGZEomm6hUM4hvCK*blDr+D z3cS?^2%BajnZ0Zkc0WF>MovOP`9Mwl*`f{QT_;iiyzOFb*e!3PwgTVmD&y{mMBBrlsEK0gvg#onyxiGLc zADZ(VpGll4!W#5lA?`Hz5F9+AAU#Y12p7qeHzucxIiy-9ftE$lgQr`7oKTn~*<>6^ zISu}2eN%5!iYWgv$0M+nujxN?&SOIE+&^Er_+b$_xJI{1u2H`QAtoz@8c~CH$3&33 z`kPbK_b)eV+)5#e749VKbcSem5ljvqvbspECn#qf+KvS{nfxlF`!l>S`l)Map8e-(M; zzU&ITUNia4w@C35EH@GkOn+99{@Bm?9oU)aueWnx^4a>n9*KzomQmcOw0zv$rG{P zw}9HVo462UlDU^~;|oNSQLyP};~mP=*qqUD;xEH|hs;cE*B|uBA)U@kg$IXc zF}UnvQG)|CvA{Bol{fO>c3myLlIO0N1IEX^G)HOIvZS|1ceZKM+~-HLM~aQ)SWtoa zDB55*nHTDCmO;MRBh8p-##TdiIskh_Q{Z-OG>xG!FlR>(nRf$-wi#Y^|~v+ zOc#Q`W~|bcL=g5jln>6BnF3J&Pe8E0&_|vXx<7)yYw<{FntS+?$ewMOo=cw>S;xm% zZDIVZAGo5vNhs+o>=^Kv_3eYQSgb0&7_lQ=MKKJe?~EIfW>^k3zQ?ZX8%uTcORT)z z6`hqTN4Wspna4o}Xe6DhH6FgE=pNTsrXpS9n+rY26vSVc`2P1H(@X6c`805D;34)1)lAY9uN+51Q z=g-;P^k^%enARg1*bFpCxD21KL5=hN!HEmM65)J>By+` z!|uOc`eyGT%Lo6H6iHX#Lh1e4{K{h$0_?yAmqO`qT2VM4*7lXO$=zuruL zy?uWlyHK$TYE1g)`AzvzLGNS#ouru~`~MD|$Pf9lzs^NWqr?p_k5kNmdF>&UVo-Af znt3nKV9eb}AMns4{2jzi|0VNE@RP!DaKKJ4?keWdn_VtL$>OIEZ0m?m84 zfdMrlbsQN6bhgN5mM1PL1tFW zqE|tE?<~{XI6>U+EVfVLSN?#Xzx;O zPjBn1iRp{_&&0k>0@fykq`pna+guG=@$n-*?wNalmle;ydH=H<2)xOE!5V@+(+|?> z;r|J*ruhu+q<~w(oa2`UfvyX`T!&*n#q;l_1!XeFT%w%dcMA9; zQU?icN_|lNNLFC&BTpj!2;?tZ!R&pyJ8J&ye|0Xkat%f)5XTo=Il=o|cRrM-KT?uA zaW81dqCMtwKa~+Fx?89E=i6f%YfgT9zAH;iM*TNWF?NF!YJ>8x99^V%lUq=?u7NTa zo1Zxc!H82?DC#H3(NOaXVN*S*2tBBB;~lsEaMpKqBziX|bi2P09DPh!yQIhd$mvx! z{1NS?!#&1z{ay=ej8bD_>&pLtQa-IeZ@8xpqvNHl0~%bOUZ@+RrL=86b$D4v_MC~s zUR+K>2xaQF^98g^LDAoWJET+Tx^48@16bFNpL8ocmqq~^8Mg#8)+&~}zb#E4R;$KL z|8u<>bW-{@l~`4Zos;|8leTSu6u{1GN*2^;B1oNe;*cc=T;;gUZ-XFdFTxz-qt#}n zNK=Qbi#5Q^kxyj=)a9q{Yx9tx5oJ&i><9elo*ZtVLyCMf*MYE-71AXBbiBRih+E+- zeJnrY*`3tl*jon>zCv?-~)VLbqlF+d%t9SHq&)3eBJ4ZUQgt;rm(@Y+l%gUgH1O!VlkH3I6AV8mv|<#D3SE zC>Dz{`&*ek-bRsZe%IbSSAC9?#)1(pD5n3Pz+-l?P?iI1&rVoS=TZkucPGz+xzVZD z61YNJ&w^!U691^J3S}w+E7x!6ie%st)c*`@8|q$o`A+q<{R)D`ahW&n{BcH7dDb;q z+1ZQme^`49u)3NhU3de*-Q5Wm+}$A%f_w1b4#C|eI0S+_K|*kMhfN6X?(S{@?%qkh z@0@ez&ir#`{-^V-daJs+y4Tv>y=ql;l04QZgHg|)#f|hW3EjnrzFimz&Xb?-h*IX0 zK_&XFO^Q`QaTKT=0VhAZ+y4>E+Muna%xl^iwH|R^a;4mLr_{6B87;dQTMjN>zqJ9E z*9CPgBd>pNp5HL<;Q59>PcW}Wd#-DkIR<1Z<3e6C0fFc)8XY?J85-UVFauqk71dU(}LtS^JuHPuws( zD{zZGHTn!-IeTzL@5P-lG+rwxO+CYNxAid~SP6*#fHO)WH^HO+VP4;d$XgZCn(XEfiA4E;%GIA}5~9w@eV)m9dz?PARAT&C9L8tp+?8Ij}rR!Onvy$OAZ0m&t; ze&gzfJk9XF#tEP6omh}(9R!A)I7U}G=#UTchq?*l1(c_F_GRRl?0D8U=4MrcIJQ8i z2Z-JhF%TWT3g;aw&&Matms;WFSzBKn>l}ALbDI9*iL9~ecfb&7W+;HTp@lb6<{c37O6x<}7$ z`IZcKHIXxpf3bTgf$0oR+5AWuKHX>y;;ShB{4b6{V6@Ia_MJ*7NeBuGic^}nV_a9) zyAtRm0!FB)&DA3w4A^(De5<$)f&PWuI^KoEZjA^5T6NDj9&xiZ&+jhNYUnxEJG>?S zICx6eIzKEX3iufaFJ)!DKLc!Vn&Mfq@kf_nopOZXp;951Bt;Y`8{9DkX1M9ed5u>I zNM(tZdK|s`G5Xjy8q?g$>~y=_Oa)vqN3I0l;*CV^*sk+NKpF^C|6GI;Fl%!GYr(n= z3(hmxRe0w;xG}lGY5ACX>du^5V-p2Mn)NyEoavmR{_+(1*&kr%glB04s(oY67XSN% zQefeb)L7sKe&l~e;6_j$VDU%1yFkPEpA*3@_9ll9f>OtJQ-t8*-RuY*{Xcqpo}~Qm z+?eP8A2$X&5;FWG+UJ;c`s05?Von)mg@JCzn3;3`UxX*~an3_X_wCFD(&QnPvEmKN zu;hP_KQ4%NOa)+$=>_)wZ`Qb>FrqGpokTm60-CD zSI?$6#tN@jd{gffL@zl{cWO02uA%!SDG37mGf~;kDGr??vnRG|2SG;ZrgwJTT6n#m zXn*EtF+Jse3>}-W+pr&V8e6chJTHtOo41FUMsH_QObL?G9ofA{cGDYvoQtHEN_Yl4 zKng#Dr%yo+P^nKc(0wB+R8k0*tg9@uAtawqC15zDg= z_FSC_Os8uq(&D=WLpaY(dW*%_vLdd6s5232AN0I0w#XInUS(t4B-*Ed9H8L`(5br{ zaA>Y}sED2;W#RI=@iMk0CfPw7R;!x`y%aW}d68nkC_78Io?1?|Xs44hSesypArw=fG+ZL9$*nHG=uG z*-mDIz}cr?TpΝ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 Prefabs.GetRandom(Rand.RandSync.ServerAndClient); + public static WreckAIConfig GetRandom() => Prefabs.OrderBy(p => p.UintIdentifier).GetRandom(Rand.RandSync.ServerAndClient); protected override Identifier DetermineIdentifier(XElement element) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 7700016df..22f35f724 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -94,11 +94,67 @@ namespace Barotrauma public Vector2 SheetIndex => Preset.SheetIndex; - public ContentXElement HairElement => CharacterInfo.Hairs?.ElementAtOrDefault(HairIndex); - public ContentXElement HairWithHatElement => CharacterInfo.Hairs?.ElementAtOrDefault(HairWithHatIndex); - public ContentXElement BeardElement => CharacterInfo.Beards?.ElementAtOrDefault(BeardIndex); - public ContentXElement MoustacheElement => CharacterInfo.Moustaches?.ElementAtOrDefault(MoustacheIndex); - public ContentXElement FaceAttachment => CharacterInfo.FaceAttachments?.ElementAtOrDefault(FaceAttachmentIndex); + public ContentXElement HairElement + { + get + { + if (CharacterInfo.Hairs == null) { return null; } + if (hairIndex >= CharacterInfo.Hairs.Count) + { + DebugConsole.AddWarning($"Hair index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {hairIndex})"); + } + return CharacterInfo.Hairs.ElementAtOrDefault(hairIndex); + } + } + public ContentXElement HairWithHatElement + { + get + { + if (CharacterInfo.Hairs == null) { return null; } + if (HairWithHatIndex >= CharacterInfo.Hairs.Count) + { + DebugConsole.AddWarning($"Hair with hat index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {HairWithHatIndex})"); + } + return CharacterInfo.Hairs.ElementAtOrDefault(HairWithHatIndex); + } + } + + public ContentXElement BeardElement + { + get + { + if (CharacterInfo.Beards == null) { return null; } + if (BeardIndex >= CharacterInfo.Beards.Count) + { + DebugConsole.AddWarning($"Beard index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {BeardIndex})"); + } + return CharacterInfo.Beards.ElementAtOrDefault(BeardIndex); + } + } + public ContentXElement MoustacheElement + { + get + { + if (CharacterInfo.Moustaches == null) { return null; } + if (MoustacheIndex >= CharacterInfo.Moustaches.Count) + { + DebugConsole.AddWarning($"Moustache index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {MoustacheIndex})"); + } + return CharacterInfo.Moustaches.ElementAtOrDefault(MoustacheIndex); + } + } + public ContentXElement FaceAttachment + { + get + { + if (CharacterInfo.FaceAttachments == null) { return null; } + if (FaceAttachmentIndex >= CharacterInfo.FaceAttachments.Count) + { + DebugConsole.AddWarning($"Face attachment index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {FaceAttachmentIndex})"); + } + return CharacterInfo.FaceAttachments.ElementAtOrDefault(FaceAttachmentIndex); + } + } public HeadInfo(CharacterInfo characterInfo, HeadPreset headPreset, int hairIndex = 0, int beardIndex = 0, int moustacheIndex = 0, int faceAttachmentIndex = 0) { @@ -130,6 +186,10 @@ namespace Barotrauma head = value; HeadSprite = null; AttachmentSprites = null; + hairs = null; + beards = null; + moustaches = null; + faceAttachments = null; } } } @@ -843,7 +903,14 @@ namespace Barotrauma public void RecreateHead(ImmutableHashSet tags, int hairIndex, int beardIndex, int moustacheIndex, int faceAttachmentIndex) { HeadPreset headPreset = Prefab.Heads.FirstOrDefault(h => h.TagSet.SetEquals(tags)); - if (headPreset == null) { headPreset = Prefab.Heads.GetRandomUnsynced(); } + if (headPreset == null) + { + if (tags.Count == 1) + { + headPreset = Prefab.Heads.FirstOrDefault(h => h.TagSet.Contains(tags.First())); + } + headPreset ??= Prefab.Heads.GetRandomUnsynced(); + } head = new HeadInfo(this, headPreset, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); ReloadHeadAttachments(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 98761b381..e958f391f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -352,7 +352,7 @@ namespace Barotrauma public Vector2 Position { - get { return ConvertUnits.ToDisplayUnits(body.SimPosition); } + get { return ConvertUnits.ToDisplayUnits(body?.SimPosition ?? Vector2.Zero); } } public Vector2 SimPosition diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 967927105..5b45d8882 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -574,7 +574,7 @@ namespace Barotrauma public float AggressionGreed { get; private set; } [Serialize(0f, IsPropertySaveable.Yes, description: "If the health drops below this threshold, the character flees. In percentages."), Editable(minValue: 0f, maxValue: 100f)] - public float FleeHealthThreshold { get; private set; } + public float FleeHealthThreshold { get; set; } [Serialize(false, IsPropertySaveable.Yes, description: "Does the character attack when provoked? When enabled, overrides the predefined targeting state with Attack and increases the priority of it."), Editable()] public bool AttackWhenProvoked { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 3369bb456..01084658f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -1122,7 +1122,7 @@ namespace Barotrauma { var gamesession = new GameSession( SubmarineInfo.SavedSubmarines.GetRandomUnsynced(s => s.Type == SubmarineType.Player && !s.HasTag(SubmarineTag.HideInMenus)), - GameModePreset.DevSandbox); + GameModePreset.DevSandbox ?? GameModePreset.Sandbox); string seed = ToolBox.RandomSeed(16); gamesession.StartRound(seed); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 94123f36e..7e634941e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -33,13 +33,13 @@ namespace Barotrauma public enum AbilityEffectType { Undefined, - None, + None, OnAttack, OnAttackResult, OnAttacked, OnAttackedResult, - OnGainSkillPoint, - OnAllyGainSkillPoint, + OnGainSkillPoint, + OnAllyGainSkillPoint, OnRepairComplete, OnItemFabricationSkillGain, OnItemFabricatedAmount, @@ -155,4 +155,10 @@ namespace Barotrauma Player = 0b10, Both = Bot | Player } -} + + public enum NumberType + { + Int, + Float + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index c3b9fad93..27748f54d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -313,10 +313,14 @@ namespace Barotrauma bool isValid = e is Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated && (e == Character.Controlled || character.IsRemotePlayer); #if SERVER - UpdateIgnoredClients(); - isValid &= !ignoredClients.Keys.Any(c => c.Character == e); + if (!dialogOpened) + { + UpdateIgnoredClients(); + isValid &= !ignoredClients.Keys.Any(c => c.Character == e); + } #elif CLIENT - isValid &= (e != Character.Controlled || !GUI.InputBlockingMenuOpen); + bool block = GUI.InputBlockingMenuOpen && !dialogOpened; + isValid &= (e != Character.Controlled || !block); #endif return isValid; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index c51ad855c..f861c2d10 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -135,8 +135,10 @@ namespace Barotrauma monster.Enabled = false; if (monster.Params.AI != null && monster.Params.AI.EnforceAggressiveBehaviorForMissions) { + monster.Params.AI.FleeHealthThreshold = 0; foreach (var targetParam in monster.Params.AI.Targets) { + if (targetParam.Tag.Equals("engine", StringComparison.OrdinalIgnoreCase)) { continue; } switch (targetParam.State) { case AIState.Avoid: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index 49631f810..8054a79ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -12,6 +12,7 @@ namespace Barotrauma private readonly Dictionary> cachedTargets = new Dictionary>(); private int prevEntityCount; private int prevPlayerCount, prevBotCount; + private Character prevControlled; private readonly string[] requiredDestinationTypes; public readonly bool RequireBeaconStation; @@ -163,12 +164,13 @@ namespace Barotrauma botCount++; } } - if (Entity.EntityCount != prevEntityCount || botCount != prevBotCount || playerCount != prevPlayerCount) + if (Entity.EntityCount != prevEntityCount || botCount != prevBotCount || playerCount != prevPlayerCount || prevControlled != Character.Controlled) { cachedTargets.Clear(); prevEntityCount = Entity.EntityCount; prevBotCount = botCount; prevPlayerCount = playerCount; + prevControlled = Character.Controlled; } if (!Actions.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs index d79130b93..ac9e379a4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs @@ -339,11 +339,6 @@ namespace Barotrauma loadContext = null; assembly = null; } - - ~Implementation() - { - OnQuit(); - } } private static Implementation? loadedImplementation; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index a421a0be1..5d15cbed9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -1,9 +1,8 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using System; using System.Collections.Generic; using System.Linq; -using Barotrauma.Extensions; -using Microsoft.Xna.Framework; namespace Barotrauma { @@ -15,29 +14,29 @@ namespace Barotrauma { if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; } - bool skipMainSubs = GameMain.GameSession.GameMode is CampaignMode { IsFirstRound: false }; - if (!skipMainSubs) + //player has more than one sub = we must have given the start items already + bool startItemsGiven = GameMain.GameSession?.OwnedSubmarines != null && GameMain.GameSession.OwnedSubmarines.Count > 1; + if (!startItemsGiven) { - if (Submarine.MainSub is Submarine mainSub && mainSub.Info.IsPlayer) - { - SpawnStartItems(mainSub); - } for (int i = 0; i < Submarine.MainSubs.Length; i++) { var sub = Submarine.MainSubs[i]; - if (sub == null || sub.Info.InitialSuppliesSpawned) { continue; } + if (sub == null || sub.Info.InitialSuppliesSpawned || !sub.Info.IsPlayer) { continue; } + //1st pass: items defined in the start item set, only spawned in the main sub (not drones/shuttles or other linked subs) + SpawnStartItems(sub); + //2nd pass: items defined using preferred containers, spawned in the main sub and all the linked subs (drones, shuttles etc) var subs = sub.GetConnectedSubs().Where(s => s.TeamID == sub.TeamID); CreateAndPlace(subs); subs.ForEach(s => s.Info.InitialSuppliesSpawned = true); } } + //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 || - sub.Info.Type == SubmarineType.EnemySubmarine) + sub.Info.Type == SubmarineType.OutpostModule) { continue; } @@ -64,6 +63,10 @@ namespace Barotrauma } public static Identifier StartItemSet = new Identifier("normal"); + + ///

+ /// Spawns the items defined in the start item set in the specified sub. + /// private static void SpawnStartItems(Submarine sub) { if (!Barotrauma.StartItemSet.Sets.TryGet(StartItemSet, out StartItemSet itemSet)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 9abdd2721..42a9dc8d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -1081,15 +1081,10 @@ namespace Barotrauma { bool hasNewPendingSub = Campaign.PendingSubmarineSwitch != null && Campaign.PendingSubmarineSwitch.MD5Hash.StringRepresentation != Submarine.Info.MD5Hash.StringRepresentation; - if (hasNewPendingSub) { Campaign.SwitchSubs(); } - else - { - SubmarineInfo = new SubmarineInfo(Submarine); - } } rootElement.Add(new XAttribute("submarine", SubmarineInfo == null ? "" : SubmarineInfo.Name)); if (OwnedSubmarines != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index d1371c9fb..405b6a22a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -621,7 +621,7 @@ namespace Barotrauma.Items.Components hullRects[i].X -= expand; hullRects[i].Width += expand * 2; hullRects[i].Location -= MathUtils.ToPoint(subs[i].WorldPosition - subs[i].HiddenSubPosition); - hulls[i] = new Hull(MapEntityPrefab.Find(null, "hull"), hullRects[i], subs[i]); + hulls[i] = new Hull(hullRects[i], subs[i]); hulls[i].RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch"; hulls[i].AddToGrid(subs[i]); hulls[i].FreeID(); @@ -744,7 +744,7 @@ namespace Barotrauma.Items.Components hullRects[i].Y += expand; hullRects[i].Height += expand * 2; hullRects[i].Location -= MathUtils.ToPoint(subs[i].WorldPosition - subs[i].HiddenSubPosition); - hulls[i] = new Hull(MapEntityPrefab.Find(null, "hull"), hullRects[i], subs[i]); + hulls[i] = new Hull(hullRects[i], subs[i]); hulls[i].RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch"; hulls[i].AddToGrid(subs[i]); hulls[i].FreeID(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs index 0467429ea..4ad251c17 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs @@ -3,7 +3,6 @@ using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 339d218aa..998d7d7c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -79,7 +79,7 @@ namespace Barotrauma.Items.Components { get { - Matrix bodyTransform = Matrix.CreateRotationZ(item.body == null ? MathHelper.ToRadians(item.Rotation) : item.body.Rotation); + Matrix bodyTransform = Matrix.CreateRotationZ(item.body == null ? item.RotationRad : item.body.Rotation); Vector2 flippedPos = barrelPos; if (item.body != null && item.body.Dir < 0.0f) { flippedPos.X = -flippedPos.X; } return Vector2.Transform(flippedPos, bodyTransform) * item.Scale; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 3d5f79b0f..903905ec6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -17,11 +17,13 @@ namespace Barotrauma.Items.Components public readonly Item Item; public readonly StatusEffect StatusEffect; public readonly bool ExcludeBroken; - public ActiveContainedItem(Item item, StatusEffect statusEffect, bool excludeBroken) + public readonly bool ExcludeFullCondition; + public ActiveContainedItem(Item item, StatusEffect statusEffect, bool excludeBroken, bool excludeFullCondition) { Item = item; StatusEffect = statusEffect; ExcludeBroken = excludeBroken; + ExcludeFullCondition = excludeFullCondition; } } @@ -300,7 +302,7 @@ namespace Barotrauma.Items.Components if (!containableItem.MatchesItem(containedItem)) { continue; } foreach (StatusEffect effect in containableItem.statusEffects) { - activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, containableItem.ExcludeBroken)); + activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, containableItem.ExcludeBroken, containableItem.ExcludeFullCondition)); } } } @@ -408,6 +410,7 @@ namespace Barotrauma.Items.Components Item contained = activeContainedItem.Item; if (activeContainedItem.ExcludeBroken && contained.Condition <= 0.0f) { continue; } + if (activeContainedItem.ExcludeFullCondition && contained.IsFullCondition) { continue; } StatusEffect effect = activeContainedItem.StatusEffect; if (effect.HasTargetType(StatusEffect.TargetType.This)) @@ -569,7 +572,7 @@ namespace Barotrauma.Items.Components transformedItemPos += new Vector2(item.Rect.X, item.Rect.Y); if (Math.Abs(item.Rotation) > 0.01f) { - Matrix transform = Matrix.CreateRotationZ(MathHelper.ToRadians(-item.Rotation)); + Matrix transform = Matrix.CreateRotationZ(-item.RotationRad); transformedItemPos = Vector2.Transform(transformedItemPos - item.Position, transform) + item.Position; transformedItemInterval = Vector2.Transform(transformedItemInterval, transform); transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform); @@ -600,7 +603,7 @@ namespace Barotrauma.Items.Components } else { - currentRotation += MathHelper.ToRadians(-item.Rotation); + currentRotation += -item.RotationRad; } int i = 0; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs index 889969115..a3588ac5c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs @@ -161,7 +161,7 @@ namespace Barotrauma.Items.Components hullData.ReceivedWaterAmount = null; if (fromWaterDetector) { - hullData.ReceivedWaterAmount = Math.Min(sourceHull.WaterVolume / sourceHull.Volume, 1.0f); + hullData.ReceivedWaterAmount = WaterDetector.GetWaterPercentage(sourceHull); } foreach (var linked in sourceHull.linkedTo) { @@ -174,7 +174,7 @@ namespace Barotrauma.Items.Components linkedHullData.ReceivedWaterAmount = null; if (fromWaterDetector) { - linkedHullData.ReceivedWaterAmount = Math.Min(linkedHull.WaterVolume / linkedHull.Volume, 1.0f); + linkedHullData.ReceivedWaterAmount = WaterDetector.GetWaterPercentage(linkedHull); } } break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 3a8095561..7e3e1da2e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -68,6 +68,8 @@ namespace Barotrauma.Items.Components private const float ConnectedSubUpdateInterval = 1.0f; float connectedSubUpdateTimer; + private double lastReceivedSteeringSignalTime; + public bool AutoPilot { get { return autoPilot; } @@ -312,16 +314,20 @@ namespace Barotrauma.Items.Components } else if (AutoPilot) { - UpdateAutoPilot(deltaTime); - float throttle = 1.0f; - if (controlledSub != null) + //signals override autopilot for a duration of one second + if (lastReceivedSteeringSignalTime < Timing.TotalTime - 1) { - //if the sub is heading in the correct direction, throttle the speed according to the user's skill - //if it's e.g. sinking due to extra water, don't throttle, but allow emptying up the ballast completely - throttle = MathHelper.Clamp(Vector2.Dot(controlledSub.Velocity, TargetVelocity) / 100.0f, 0.0f, 1.0f); + UpdateAutoPilot(deltaTime); + float throttle = 1.0f; + if (controlledSub != null) + { + //if the sub is heading in the correct direction, throttle the speed according to the user's skill + //if it's e.g. sinking due to extra water, don't throttle, but allow emptying up the ballast completely + throttle = MathHelper.Clamp(Vector2.Dot(controlledSub.Velocity, TargetVelocity) / 100.0f, 0.0f, 1.0f); + } + float maxSpeed = MathHelper.Lerp(AutoPilotMaxSpeed, AIPilotMaxSpeed, userSkill) * 100.0f; + TargetVelocity = TargetVelocity.ClampLength(MathHelper.Lerp(100.0f, maxSpeed, throttle)); } - float maxSpeed = MathHelper.Lerp(AutoPilotMaxSpeed, AIPilotMaxSpeed, userSkill) * 100.0f; - TargetVelocity = TargetVelocity.ClampLength(MathHelper.Lerp(100.0f, maxSpeed, throttle)); } else { @@ -821,6 +827,7 @@ namespace Barotrauma.Items.Components steeringInput.X = MathHelper.Clamp(steeringInput.X, -100.0f, 100.0f); steeringInput.Y = MathHelper.Clamp(-steeringInput.Y, -100.0f, 100.0f); TargetVelocity = steeringInput; + lastReceivedSteeringSignalTime = Timing.TotalTime; } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index 977dda461..cae59479d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -150,7 +150,7 @@ namespace Barotrauma.Items.Components { if (powerOut?.Grid != null) { return powerOut.Grid.Voltage; } } - return currPowerConsumption <= 0.0f ? 1.0f : voltage; + return PowerConsumption <= 0.0f ? 1.0f : voltage; } set { @@ -158,21 +158,7 @@ namespace Barotrauma.Items.Components } } - public bool PoweredByTinkering - { - get - { - if (this is PowerContainer) { return false; } - foreach (Repairable repairable in Item.Repairables) - { - if (repairable.IsTinkering && repairable.TinkeringPowersDevices) - { - return true; - } - } - return false; - } - } + public bool PoweredByTinkering { get; set; } [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the item be damaged by electomagnetic pulses.")] public bool VulnerableToEMP diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index ee6516ef8..b72ea07b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -965,7 +965,8 @@ namespace Barotrauma.Items.Components { item.body.LinearVelocity *= deflectedSpeedMultiplier; } - else if ( stickJoint == null && StickTarget == null && + else if ( remainingHits <= 0 && + stickJoint == null && StickTarget == null && StickToStructures && target.Body.UserData is Structure || ((StickToLightTargets || target.Body.Mass > item.body.Mass * 0.5f) && (DoesStick || diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs index 0be5f9561..0d3dd454c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs @@ -56,7 +56,8 @@ namespace Barotrauma.Items.Components if (value == qualityLevel) { return; } bool wasInFullCondition = item.IsFullCondition; - qualityLevel = MathHelper.Clamp(value, 0, MaxQuality); + qualityLevel = MathHelper.Clamp(value, 0, MaxQuality); + item.RecalculateConditionValues(); //set the condition to the new max condition if (wasInFullCondition && statValues.ContainsKey(StatType.Condition)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 45d5d3ae4..edb4d6242 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -106,7 +106,25 @@ namespace Barotrauma.Items.Components } } - public bool IsTinkering { get; private set; } = false; + private bool isTinkering; + public bool IsTinkering + { + get { return isTinkering; } + private set + { + if (isTinkering == value) { return; } + isTinkering = value; + + if (tinkeringPowersDevices) + { + foreach (Powered powered in item.GetComponents()) + { + if (powered is PowerContainer) { continue; } + powered.PoweredByTinkering = isTinkering; + } + } + } + } public Character CurrentFixer { get; private set; } private Item currentRepairItem; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs index 6d1c899b3..7f0c5ca8e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -160,7 +160,8 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { if (source == null || target == null || target.Removed || - (source is Entity sourceEntity && sourceEntity.Removed)) + (source is Entity sourceEntity && sourceEntity.Removed) || + (source is Limb limb && limb.Removed)) { ResetSource(); target = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index 92149904e..2cbdc98fa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -3,6 +3,7 @@ using Barotrauma.Networking; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using System.Globalization; namespace Barotrauma.Items.Components { @@ -34,13 +35,17 @@ namespace Barotrauma.Items.Components public Identifier PropertyName { get; } public bool TargetOnlyParentProperty { get; } - public int NumberInputMin { get; } - public int NumberInputMax { get; } + public string NumberInputMin { get; } + public string NumberInputMax { get; } + public string NumberInputStep { get; } + public int NumberInputDecimalPlaces { get; } public int MaxTextLength { get; } - public const int DefaultNumberInputMin = 0, DefaultNumberInputMax = 99; - public bool IsIntegerInput { get; } + public const string DefaultNumberInputMin = "0", DefaultNumberInputMax = "99", DefaultNumberInputStep = "1"; + public const int DefaultNumberInputDecimalPlaces = 0; + public bool IsNumberInput { get; } + public NumberType? NumberType { get; } public bool HasPropertyName { get; } public bool ShouldSetProperty { get; set; } @@ -60,11 +65,34 @@ namespace Barotrauma.Items.Components ConnectionName = element.GetAttributeString("connection", ""); PropertyName = element.GetAttributeIdentifier("propertyname", ""); TargetOnlyParentProperty = element.GetAttributeBool("targetonlyparentproperty", false); - NumberInputMin = element.GetAttributeInt("min", DefaultNumberInputMin); - NumberInputMax = element.GetAttributeInt("max", DefaultNumberInputMax); + NumberInputMin = element.GetAttributeString("min", DefaultNumberInputMin); + NumberInputMax = element.GetAttributeString("max", DefaultNumberInputMax); + NumberInputStep = element.GetAttributeString("step", DefaultNumberInputStep); + NumberInputDecimalPlaces = element.GetAttributeInt("decimalplaces", DefaultNumberInputDecimalPlaces); MaxTextLength = element.GetAttributeInt("maxtextlength", int.MaxValue); + HasPropertyName = !PropertyName.IsEmpty; - IsIntegerInput = HasPropertyName && element.Name.ToString().ToLowerInvariant() == "integerinput"; + if (HasPropertyName) + { + string elementName = element.Name.ToString().ToLowerInvariant(); + IsNumberInput = elementName == "numberinput" || elementName == "integerinput"; // backwards compatibility + if (IsNumberInput) + { + string numberType = element.GetAttributeString("numbertype", string.Empty); + switch (numberType) + { + case "f": + case "float": + NumberType = Barotrauma.NumberType.Float; + break; + case "int": + case "integer": + default: // backwards compatibility + NumberType = Barotrauma.NumberType.Int; + break; + } + } + } if (element.GetAttribute("signal") is XAttribute attribute) { @@ -152,7 +180,8 @@ namespace Barotrauma.Items.Components { case "button": case "textbox": - case "integerinput": + case "integerinput": // backwards compatibility + case "numberinput": var button = new CustomInterfaceElement(item, subElement, this) { ContinuousSignal = false @@ -317,6 +346,24 @@ namespace Barotrauma.Items.Components } } + private void ValueChanged(CustomInterfaceElement numberInputElement, float value) + { + if (numberInputElement == null) { return; } + numberInputElement.Signal = value.ToString(); + if (!numberInputElement.TargetOnlyParentProperty) + { + foreach (ISerializableEntity e in item.AllPropertyObjects) + { + if (!e.SerializableProperties.ContainsKey(numberInputElement.PropertyName)) { continue; } + e.SerializableProperties[numberInputElement.PropertyName].TrySetValue(e, value); + } + } + else if (SerializableProperties.ContainsKey(numberInputElement.PropertyName)) + { + SerializableProperties[numberInputElement.PropertyName].TrySetValue(this, value); + } + } + public override void Update(float deltaTime, Camera cam) { foreach (CustomInterfaceElement ciElement in customInterfaceElementList) @@ -341,5 +388,10 @@ namespace Barotrauma.Items.Components signals = customInterfaceElementList.Select(ci => ci.Signal).ToArray(); return base.Save(parentElement); } + + private static bool TryParseFloatInvariantCulture(string s, out float f) + { + return float.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out f); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs index 7fc471035..5e70fcf9d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs @@ -64,6 +64,11 @@ namespace Barotrauma.Items.Components IsActive = true; } + public static int GetWaterPercentage(Hull hull) + { + return hull.WaterVolume > 1.0f ? MathHelper.Clamp((int)Math.Ceiling(hull.WaterPercentage), 0, 100) : 0; + } + public override void Update(float deltaTime, Camera cam) { if (stateSwitchDelay > 0.0f) @@ -103,12 +108,7 @@ namespace Barotrauma.Items.Components if (item.CurrentHull != null) { - int waterPercentage = 0; - //ignore minuscule amounts of water - if (item.CurrentHull.WaterVolume > 1.0f) - { - waterPercentage = MathHelper.Clamp((int)Math.Ceiling(item.CurrentHull.WaterPercentage), 0, 100); - } + int waterPercentage = GetWaterPercentage(item.CurrentHull); if (prevSentWaterPercentageValue != waterPercentage || waterPercentageSignal == null) { prevSentWaterPercentageValue = waterPercentage; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index a6720fe49..32750626f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -350,7 +350,7 @@ namespace Barotrauma.Items.Components if (lightComponent != null) { lightComponent.Parent = null; - lightComponent.Rotation = Rotation - MathHelper.ToRadians(item.Rotation); + lightComponent.Rotation = Rotation - item.RotationRad; lightComponent.Light.Rotation = -rotation; } #endif @@ -516,7 +516,7 @@ namespace Barotrauma.Items.Components { if (lightComponent != null) { - lightComponent.Rotation = Rotation - MathHelper.ToRadians(item.Rotation); + lightComponent.Rotation = Rotation - item.RotationRad; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 8df11b4de..8f7b0e4fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -262,19 +262,19 @@ namespace Barotrauma } } - private float rotationRad; + 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)] public float Rotation { get { - return MathHelper.ToDegrees(rotationRad); + return MathHelper.ToDegrees(RotationRad); } set { if (!Prefab.AllowRotatingInEditor) { return; } - rotationRad = MathHelper.ToRadians(value); + RotationRad = MathHelper.ToRadians(value); #if CLIENT if (Screen.Selected == GameMain.SubEditorScreen) { @@ -472,9 +472,9 @@ namespace Barotrauma get { return spriteColor; } } - public bool IsFullCondition => MathUtils.NearlyEqual(Condition, MaxCondition); - public float MaxCondition => Prefab.Health * healthMultiplier * maxRepairConditionMultiplier * (1.0f + GetQualityModifier(Items.Components.Quality.StatType.Condition)); - public float ConditionPercentage => MathUtils.Percentage(Condition, MaxCondition); + public bool IsFullCondition { get; private set; } + public float MaxCondition { get; private set; } + public float ConditionPercentage { get; private set; } private float offsetOnSelectedMultiplier = 1.0f; @@ -495,7 +495,8 @@ namespace Barotrauma { float prevConditionPercentage = ConditionPercentage; healthMultiplier = MathHelper.Clamp(value, 0.0f, float.PositiveInfinity); - Condition = MaxCondition * prevConditionPercentage / 100.0f; + condition = MaxCondition * prevConditionPercentage / 100.0f; + RecalculateConditionValues(); } } @@ -505,7 +506,11 @@ namespace Barotrauma public float MaxRepairConditionMultiplier { get => maxRepairConditionMultiplier; - set { maxRepairConditionMultiplier = MathHelper.Clamp(value, 0.0f, float.PositiveInfinity); } + set + { + maxRepairConditionMultiplier = MathHelper.Clamp(value, 0.0f, float.PositiveInfinity); + RecalculateConditionValues(); + } } //the default value should be Prefab.Health, but because we can't use it in the attribute, @@ -806,7 +811,9 @@ namespace Barotrauma defaultRect = newRect; rect = newRect; - condition = MaxCondition; + condition = MaxCondition = Prefab.Health; + ConditionPercentage = 100.0f; + lastSentCondition = condition; AllowDeconstruct = itemPrefab.AllowDeconstruct; @@ -1002,6 +1009,7 @@ namespace Barotrauma ApplyStatusEffects(ActionType.OnSpawn, 1.0f); Components.ForEach(c => c.ApplyStatusEffects(ActionType.OnSpawn, 1.0f)); + RecalculateConditionValues(); } partial void InitProjSpecific(); @@ -1184,7 +1192,6 @@ namespace Barotrauma public void RemoveContained(Item contained) { ownInventory?.RemoveItem(contained); - contained.Container = null; } @@ -1611,6 +1618,10 @@ namespace Barotrauma bool wasInFullCondition = IsFullCondition; condition = MathHelper.Clamp(value, 0.0f, MaxCondition); + if (MathUtils.NearlyEqual(prev, condition, epsilon: 0.000001f)) { return; } + + RecalculateConditionValues(); + if (condition == 0.0f && prev > 0.0f) { //Flag connections to be updated as device is broken @@ -1672,6 +1683,17 @@ namespace Barotrauma } } + /// + /// Recalculates the item's maximum condition, condition percentage and whether it's in full condition. + /// You generally never need to call this manually - done automatically when any of the factors that affect the values change. + /// + public void RecalculateConditionValues() + { + MaxCondition = Prefab.Health * healthMultiplier * maxRepairConditionMultiplier * (1.0f + GetQualityModifier(Items.Components.Quality.StatType.Condition)); + IsFullCondition = MathUtils.NearlyEqual(Condition, MaxCondition); + ConditionPercentage = MathUtils.Percentage(Condition, MaxCondition); + } + private bool IsInWater() { if (CurrentHull == null) { return true; } @@ -1999,7 +2021,7 @@ namespace Barotrauma if (Prefab.AllowRotatingInEditor) { - rotationRad = MathUtils.WrapAngleTwoPi(-rotationRad); + RotationRad = MathUtils.WrapAngleTwoPi(-RotationRad); } #if CLIENT if (Prefab.CanSpriteFlipX) @@ -3153,12 +3175,12 @@ namespace Barotrauma { Vector2 oldRelativeOrigin = (oldPrefab.SwappableItem.SwapOrigin - oldPrefab.Size / 2) * element.GetAttributeFloat(item.scale, "scale", "Scale"); oldRelativeOrigin.Y = -oldRelativeOrigin.Y; - oldRelativeOrigin = MathUtils.RotatePoint(oldRelativeOrigin, -item.rotationRad); + oldRelativeOrigin = MathUtils.RotatePoint(oldRelativeOrigin, -item.RotationRad); Vector2 oldOrigin = centerPos + oldRelativeOrigin; Vector2 relativeOrigin = (prefab.SwappableItem.SwapOrigin - prefab.Size / 2) * item.Scale; relativeOrigin.Y = -relativeOrigin.Y; - relativeOrigin = MathUtils.RotatePoint(relativeOrigin, -item.rotationRad); + relativeOrigin = MathUtils.RotatePoint(relativeOrigin, -item.RotationRad); Vector2 origin = new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + relativeOrigin; item.rect.Location -= (origin - oldOrigin).ToPoint(); @@ -3194,6 +3216,7 @@ namespace Barotrauma item.condition = MathHelper.Clamp(condition, 0, item.MaxCondition); item.lastSentCondition = item.condition; + item.RecalculateConditionValues(); item.SetActiveSprite(); if (submarine?.Info.GameVersion != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index 46ae8392c..a74a686d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -36,6 +36,11 @@ namespace Barotrauma /// public bool ExcludeBroken { get; private set; } + /// + /// Should full condition (100%) items be excluded + /// + public bool ExcludeFullCondition { get; private set; } + public bool AllowVariants { get; private set; } = true; public RelationType Type @@ -102,14 +107,14 @@ namespace Barotrauma return CheckContained(parentItem); case RelationType.Container: if (parentItem == null || parentItem.Container == null) { return MatchOnEmpty; } - return (!ExcludeBroken || parentItem.Container.Condition > 0.0f) && MatchesItem(parentItem.Container); + return (!ExcludeBroken || parentItem.Container.Condition > 0.0f) && (!ExcludeFullCondition || !parentItem.Container.IsFullCondition) && MatchesItem(parentItem.Container); case RelationType.Equipped: if (character == null) { return false; } if (MatchOnEmpty && !character.HeldItems.Any()) { return true; } foreach (Item equippedItem in character.HeldItems) { if (equippedItem == null) { continue; } - if ((!ExcludeBroken || equippedItem.Condition > 0.0f) && MatchesItem(equippedItem)) { return true; } + if ((!ExcludeBroken || equippedItem.Condition > 0.0f) && (!ExcludeFullCondition || !equippedItem.IsFullCondition) && MatchesItem(equippedItem)) { return true; } } break; case RelationType.Picked: @@ -138,8 +143,7 @@ namespace Barotrauma foreach (Item contained in parentItem.ContainedItems) { if (TargetSlot > -1 && parentItem.OwnInventory.FindIndex(contained) != TargetSlot) { continue; } - if ((!ExcludeBroken || contained.Condition > 0.0f) && MatchesItem(contained)) { return true; } - + if ((!ExcludeBroken || contained.Condition > 0.0f) && (!ExcludeFullCondition || !contained.IsFullCondition) && MatchesItem(contained)) { return true; } if (CheckContained(contained)) { return true; } } return false; @@ -153,6 +157,7 @@ namespace Barotrauma new XAttribute("optional", IsOptional), new XAttribute("ignoreineditor", IgnoreInEditor), new XAttribute("excludebroken", ExcludeBroken), + new XAttribute("excludefullcondition", ExcludeFullCondition), new XAttribute("targetslot", TargetSlot), new XAttribute("allowvariants", AllowVariants)); @@ -212,12 +217,12 @@ namespace Barotrauma } } - if (identifiers.Length == 0 && excludedIdentifiers.Length == 0 && !returnEmpty) { return null; } RelatedItem ri = new RelatedItem(identifiers, excludedIdentifiers) { ExcludeBroken = element.GetAttributeBool("excludebroken", true), + ExcludeFullCondition = element.GetAttributeBool("excludefullcondition", false), AllowVariants = element.GetAttributeBool("allowvariants", true) }; string typeStr = element.GetAttributeString("type", ""); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/StartItemSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/StartItemSet.cs new file mode 100644 index 000000000..a93ea13c3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/StartItemSet.cs @@ -0,0 +1,36 @@ +#nullable enable +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + internal class StartItem + { + public Identifier Item; + public int Amount; + + public StartItem(XElement element) + { + Item = element.GetAttributeIdentifier("identifier", Identifier.Empty); + Amount = element.GetAttributeInt("amount", 1); + } + } + + /// + /// Additive sets of items spawned only at the start of the game. + /// + internal class StartItemSet : PrefabWithUintIdentifier + { + public readonly static PrefabCollection Sets = new PrefabCollection(); + + public readonly ImmutableArray Items; + + public StartItemSet(ContentXElement element, StartItemsFile file) : base(file, element.GetAttributeIdentifier("identifier", Identifier.Empty)) + { + Items = element.Elements().Select(e => new StartItem(e!)).ToImmutableArray(); + } + + public override void Dispose() { } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs index b9fc28d5d..c5c646150 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs @@ -25,6 +25,7 @@ namespace Barotrauma IEnumerable aliases = null) : base(identifier) { + System.Diagnostics.Debug.Assert(constructor != null); this.constructor = constructor; this.Name = TextManager.Get($"EntityName.{identifier}"); this.Description = TextManager.Get($"EntityDescription.{identifier}"); @@ -35,40 +36,52 @@ namespace Barotrauma this.Aliases = (aliases ?? Enumerable.Empty()).Concat(identifier.Value.ToEnumerable()).ToImmutableHashSet(); } + public static CoreEntityPrefab HullPrefab { get; private set; } + public static CoreEntityPrefab GapPrefab { get; private set; } + public static CoreEntityPrefab WayPointPrefab { get; private set; } + public static CoreEntityPrefab SpawnPointPrefab { get; private set; } + public static void InitCorePrefabs() { - CoreEntityPrefab ep = new CoreEntityPrefab( + HullPrefab = new CoreEntityPrefab( "hull".ToIdentifier(), - typeof(Hull).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }), + typeof(Hull).GetConstructor(new Type[] { typeof(Rectangle) }), resizeHorizontal: true, resizeVertical: true, linkable: true, allowedLinks: new Identifier[] { "hull".ToIdentifier() }); - Prefabs.Add(ep, false); + Prefabs.Add(HullPrefab, false); - ep = new CoreEntityPrefab( + GapPrefab = new CoreEntityPrefab( "gap".ToIdentifier(), - typeof(Gap).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }), + typeof(Gap).GetConstructor(new Type[] { typeof(Rectangle) }), resizeHorizontal: true, resizeVertical: true); - Prefabs.Add(ep, false); + Prefabs.Add(GapPrefab, false); - ep = new CoreEntityPrefab( + WayPointPrefab = new CoreEntityPrefab( "waypoint".ToIdentifier(), typeof(WayPoint).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) })); - Prefabs.Add(ep, false); + Prefabs.Add(WayPointPrefab, false); - ep = new CoreEntityPrefab( + SpawnPointPrefab = new CoreEntityPrefab( "spawnpoint".ToIdentifier(), typeof(WayPoint).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) })); - Prefabs.Add(ep, false); + Prefabs.Add(SpawnPointPrefab, false); } protected override void CreateInstance(Rectangle rect) { - if (constructor == null) return; - object[] lobject = new object[] { this, rect }; - constructor.Invoke(lobject); + if (this == WayPointPrefab || this == SpawnPointPrefab) + { + object[] lobject = new object[] { this, rect }; + constructor.Invoke(lobject); + } + else + { + object[] lobject = new object[] { rect }; + constructor.Invoke(lobject); + } } private bool disposed = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 53c2a59eb..291737a4e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -120,7 +120,7 @@ namespace Barotrauma } } - public Gap(MapEntityPrefab prefab, Rectangle rectangle) + public Gap(Rectangle rectangle) : this(rectangle, Submarine.MainSub) { #if CLIENT @@ -136,7 +136,7 @@ namespace Barotrauma { } public Gap(Rectangle rect, bool isHorizontal, Submarine submarine, ushort id = Entity.NullEntityID) - : base(MapEntityPrefab.FindByIdentifier("gap".ToIdentifier()), submarine, id) + : base(CoreEntityPrefab.GapPrefab, submarine, id) { this.rect = rect; flowForce = Vector2.Zero; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 88cfa4d2c..9702e90be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -410,8 +410,8 @@ namespace Barotrauma public BallastFloraBehavior BallastFlora { get; set; } - public Hull(MapEntityPrefab prefab, Rectangle rectangle) - : this (prefab, rectangle, Submarine.MainSub) + public Hull(Rectangle rectangle) + : this (rectangle, Submarine.MainSub) { #if CLIENT if (SubEditorScreen.IsSubEditor()) @@ -421,8 +421,8 @@ namespace Barotrauma #endif } - public Hull(MapEntityPrefab prefab, Rectangle rectangle, Submarine submarine, ushort id = Entity.NullEntityID) - : base (prefab, submarine, id) + public Hull(Rectangle rectangle, Submarine submarine, ushort id = Entity.NullEntityID) + : base (CoreEntityPrefab.HullPrefab, submarine, id) { rect = rectangle; @@ -500,7 +500,7 @@ namespace Barotrauma public override MapEntity Clone() { - var clone = new Hull(MapEntityPrefab.FindByIdentifier("hull".ToIdentifier()), rect, Submarine); + var clone = new Hull(rect, Submarine); foreach (KeyValuePair property in SerializableProperties) { if (!property.Value.Attributes.OfType().Any()) { continue; } @@ -1543,7 +1543,7 @@ namespace Barotrauma int.Parse(element.GetAttribute("height").Value)); } - var hull = new Hull(MapEntityPrefab.Find(null, "hull"), rect, submarine, idRemap.GetOffsetId(element)) + var hull = new Hull(rect, submarine, idRemap.GetOffsetId(element)) { WaterVolume = element.GetAttributeFloat("pressure", 0.0f) }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs index 1205fa136..298638c46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Xml.Linq; namespace Barotrauma { @@ -14,6 +11,8 @@ namespace Barotrauma public readonly LocalizedString Description; public readonly bool IsEndBiome; + public readonly float MinDifficulty; + public readonly float MaxDifficulty; public readonly ImmutableHashSet AllowedZones; @@ -30,8 +29,9 @@ namespace Barotrauma element.GetAttributeString("description", "")); IsEndBiome = element.GetAttributeBool("endbiome", false); - AllowedZones = element.GetAttributeIntArray("AllowedZones", new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }).ToImmutableHashSet(); + MinDifficulty = element.GetAttributeFloat("MinDifficulty", 0); + MaxDifficulty = element.GetAttributeFloat("MaxDifficulty", 100); } public static Identifier ParseIdentifier(ContentXElement element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs index 4adb2834f..b71326832 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs @@ -96,24 +96,31 @@ namespace Barotrauma public readonly Sprite WallSprite; public readonly Sprite WallEdgeSprite; - public static CaveGenerationParams GetRandom(LevelGenerationParams generationParams, bool abyss, Rand.RandSync rand) + public static CaveGenerationParams GetRandom(Level level, bool abyss, Rand.RandSync rand) { var caveParams = CaveParams.OrderBy(p => p.UintIdentifier).ToList(); - if (caveParams.All(p => p.GetCommonness(generationParams, abyss) <= 0.0f)) + if (caveParams.All(p => p.GetCommonness(level.LevelData, abyss) <= 0.0f)) { return caveParams.First(); } - return ToolBox.SelectWeightedRandom(caveParams.ToList(), caveParams.Select(p => p.GetCommonness(generationParams, abyss)).ToList(), rand); + return ToolBox.SelectWeightedRandom(caveParams.ToList(), caveParams.Select(p => p.GetCommonness(level.LevelData, abyss)).ToList(), rand); } - public float GetCommonness(LevelGenerationParams generationParams, bool abyss) + public float GetCommonness(LevelData levelData, bool abyss) { - if (generationParams != null && - generationParams.Identifier != Identifier.Empty && - OverrideCommonness.TryGetValue(abyss ? "abyss".ToIdentifier() : generationParams.Identifier, out float commonness)) + if (levelData.GenerationParams != null && levelData.GenerationParams.Identifier != Identifier.Empty && + OverrideCommonness.TryGetValue(abyss ? "abyss".ToIdentifier() : levelData.GenerationParams.Identifier, out float commonness)) { return commonness; } + if (levelData?.Biome != null) + { + if (OverrideCommonness.TryGetValue(levelData.Biome.Identifier, out float biomeCommonness)) + { + return biomeCommonness; + } + } + return Commonness; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 8b1831507..61049099d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -442,6 +442,11 @@ namespace Barotrauma Loaded?.Remove(); Loaded = this; Generating = true; +#if CLIENT + Debug.Assert(GenerationParams.Identifier != "coldcavernstutorial" || GameMain.GameSession?.GameMode == null || GameMain.GameSession.GameMode is TutorialMode); +#endif + Debug.Assert(GenerationParams.AnyBiomeAllowed || GenerationParams.AllowedBiomeIdentifiers.Contains(LevelData.Biome.Identifier)); + DebugConsole.NewMessage("Level identifier: " + GenerationParams.Identifier); ClearEqualityCheckValues(); EntitiesBeforeGenerate = GetEntities().ToList(); @@ -1711,7 +1716,8 @@ namespace Barotrauma else { //if the bottom of the abyss area is below crush depth, try to move it up to keep (most) of the abyss content above crush depth - if (abyssEndY + CrushDepth < 0) + //but only if start of the abyss is above crush depth (no point in doing this if all of it is below crush depth) + if (abyssEndY + CrushDepth < 0 && abyssStartY > -CrushDepth) { abyssEndY += Math.Min(-(abyssEndY + (int)CrushDepth), abyssHeight / 2); } @@ -1820,7 +1826,7 @@ namespace Barotrauma } } - var caveParams = CaveGenerationParams.GetRandom(GenerationParams, abyss: true, rand: Rand.RandSync.ServerAndClient); + var caveParams = CaveGenerationParams.GetRandom(this, abyss: true, rand: Rand.RandSync.ServerAndClient); float caveScaleRelativeToIsland = 0.7f; GenerateCave( @@ -1889,7 +1895,7 @@ namespace Barotrauma { for (int i = 0; i < GenerationParams.CaveCount; i++) { - var caveParams = CaveGenerationParams.GetRandom(GenerationParams, abyss: false, rand: Rand.RandSync.ServerAndClient); + var caveParams = CaveGenerationParams.GetRandom(this, abyss: false, rand: Rand.RandSync.ServerAndClient); Point caveSize = new Point( Rand.Range(caveParams.MinWidth, caveParams.MaxWidth, Rand.RandSync.ServerAndClient), Rand.Range(caveParams.MinHeight, caveParams.MaxHeight, Rand.RandSync.ServerAndClient)); @@ -2479,6 +2485,7 @@ namespace Barotrauma foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs.OrderBy(p => p.UintIdentifier)) { if (itemPrefab.LevelCommonness.TryGetValue(levelName, out float commonness) || + itemPrefab.LevelCommonness.TryGetValue(LevelData.Biome.Identifier, out commonness) || itemPrefab.LevelCommonness.TryGetValue(Identifier.Empty, out commonness)) { if (commonness <= 0.0f) { continue; } @@ -3237,7 +3244,8 @@ namespace Barotrauma if (index < 0 || index >= bottomPositions.Count - 1) { return new Vector2(xPosition, BottomPos); } float t = (xPosition - bottomPositions[index].X) / (bottomPositions[index + 1].X - bottomPositions[index].X); - Debug.Assert(t <= 1.0f); + //t can go slightly outside the 0-1 due to rounding, safe to ignore + Debug.Assert(t <= 1.001f && t >= -0.001f); t = MathHelper.Clamp(t, 0.0f, 1.0f); float yPos = MathHelper.Lerp(bottomPositions[index].Y, bottomPositions[index + 1].Y, t); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 7313759d5..10f625e3b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -20,7 +20,7 @@ namespace Barotrauma public readonly string Seed; - public readonly float Difficulty; + public float Difficulty; public readonly Biome Biome; @@ -90,10 +90,10 @@ namespace Barotrauma (int)MathUtils.Round(generationParams.Height, Level.GridCellSize)); } - public LevelData(XElement element) + public LevelData(XElement element, float? forceDifficulty = null) { Seed = element.GetAttributeString("seed", ""); - Difficulty = element.GetAttributeFloat("difficulty", 0.0f); + Difficulty = forceDifficulty ?? element.GetAttributeFloat("difficulty", 0.0f); Size = element.GetAttributePoint("size", new Point(1000)); Enum.TryParse(element.GetAttributeString("type", "LocationConnection"), out Type); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index bc380746d..3742ade58 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -414,7 +414,7 @@ namespace Barotrauma set; } - [Serialize(50, IsPropertySaveable.Yes, description: "Maximum number of resource clusters in the abyss (the actual number is picked between min and max according to the level difficulty)"), Editable(MinValueInt = 0, MaxValueInt = 1000)] + [Serialize(40, IsPropertySaveable.Yes, description: "Maximum number of resource clusters in the abyss (the actual number is picked between min and max according to the level difficulty)"), Editable(MinValueInt = 0, MaxValueInt = 1000)] public int AbyssResourceClustersMax { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index b07e2dd78..8e30125de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -170,7 +170,7 @@ namespace Barotrauma for (int i = 0; i < amount; i++) { //get a random prefab and find a place to spawn it - LevelObjectPrefab prefab = GetRandomPrefab(level.GenerationParams, availablePrefabs); + LevelObjectPrefab prefab = GetRandomPrefab(level, availablePrefabs); if (prefab == null) { continue; } if (!suitableSpawnPositions.ContainsKey(prefab)) { @@ -595,12 +595,12 @@ namespace Barotrauma } } - private LevelObjectPrefab GetRandomPrefab(LevelGenerationParams generationParams, IList availablePrefabs) + private LevelObjectPrefab GetRandomPrefab(Level level, IList availablePrefabs) { - if (availablePrefabs.Sum(p => p.GetCommonness(generationParams)) <= 0.0f) { return null; } + if (availablePrefabs.Sum(p => p.GetCommonness(level.LevelData)) <= 0.0f) { return null; } return ToolBox.SelectWeightedRandom( availablePrefabs, - availablePrefabs.Select(p => p.GetCommonness(generationParams)).ToList(), Rand.RandSync.ServerAndClient); + availablePrefabs.Select(p => p.GetCommonness(level.LevelData)).ToList(), Rand.RandSync.ServerAndClient); } private LevelObjectPrefab GetRandomPrefab(CaveGenerationParams caveParams, IList availablePrefabs, bool requireCaveSpecificOverride) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs index 326b443ca..a2ad86140 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs @@ -426,15 +426,21 @@ namespace Barotrauma return requireCaveSpecificOverride ? 0.0f : Commonness; } - public float GetCommonness(LevelGenerationParams generationParams) - { - if (generationParams != null && - generationParams.Identifier != Identifier.Empty && - (OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness) || - (!generationParams.OldIdentifier.IsEmpty && OverrideCommonness.TryGetValue(generationParams.OldIdentifier, out commonness)))) + public float GetCommonness(LevelData levelData) + { + if (levelData.GenerationParams != null && levelData.GenerationParams.Identifier != Identifier.Empty && + OverrideCommonness.TryGetValue(levelData.GenerationParams.Identifier, out float commonness) || + (!levelData.GenerationParams.OldIdentifier.IsEmpty && OverrideCommonness.TryGetValue(levelData.GenerationParams.OldIdentifier, out commonness))) { return commonness; } + if (levelData?.Biome != null) + { + if (OverrideCommonness.TryGetValue(levelData.Biome.Identifier, out float biomeCommonness)) + { + return biomeCommonness; + } + } return Commonness; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs index 67cb59004..8b3c2c297 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs @@ -152,12 +152,6 @@ namespace Barotrauma public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { #if CLIENT VertexBuffer?.Dispose(); VertexBuffer = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index e17f274bf..08ac7d07d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -485,6 +485,19 @@ namespace Barotrauma TurnsInRadiation = element.GetAttributeInt(nameof(TurnsInRadiation).ToLower(), 0); StepsSinceSpecialsUpdated = element.GetAttributeInt("stepssincespecialsupdated", 0); + Identifier biomeId = element.GetAttributeIdentifier("biome", Identifier.Empty); + if (biomeId != Identifier.Empty) + { + if (Biome.Prefabs.TryGet(biomeId, out Biome biome)) + { + Biome = biome; + } + else + { + DebugConsole.ThrowError($"Error while loading the campaign map: could not find a biome with the identifier \"{biomeId}\"."); + } + } + if (!typeNotFound) { for (int i = 0; i < Type.CanChangeTo.Count; i++) @@ -773,22 +786,41 @@ namespace Barotrauma static float GetConnectionWeight(Location location, LocationConnection c) { - float weight = c.Passed ? 1.0f : 5.0f; Location destination = c.OtherLocation(location); - if (destination != null) + if (destination == null) { return 0; } + float minWeight = 0.0001f; + float lowWeight = 0.2f; + float normalWeight = 1.0f; + float maxWeight = 2.0f; + float weight = c.Passed ? lowWeight : normalWeight; + if (location.Biome.AllowedZones.Contains(1)) { - if (destination.MapPosition.X > location.MapPosition.X) { weight *= 2.0f; } - int missionCount = location.availableMissions.Count(m => m.Locations.Contains(destination)); - if (missionCount > 0) - { - weight /= missionCount * 2; - } - if (destination.IsRadiated()) + // In the first biome, give a stronger preference for locations that are farther to the right) + float diff = destination.MapPosition.X - location.MapPosition.X; + if (diff < 0) { - weight *= 0.001f; + weight *= 0.1f; + } + else + { + float maxRelevantDiff = 300; + weight = MathHelper.Lerp(weight, maxWeight, MathUtils.InverseLerp(0, maxRelevantDiff, diff)); } } - return weight; + else if (destination.MapPosition.X > location.MapPosition.X) + { + weight *= 2.0f; + } + int missionCount = location.availableMissions.Count(m => m.Locations.Contains(destination)); + if (missionCount > 0) + { + weight /= missionCount * 2; + } + if (destination.IsRadiated()) + { + weight *= 0.001f; + } + return MathHelper.Clamp(weight, minWeight, maxWeight); } return InstantiateMission(prefab, connection); @@ -1255,6 +1287,7 @@ namespace Barotrauma new XAttribute("originaltype", (Type ?? OriginalType).Identifier), new XAttribute("basename", BaseName), new XAttribute("name", Name), + new XAttribute("biome", Biome?.Identifier.Value ?? string.Empty), new XAttribute("discovered", Discovered), new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)), new XAttribute("pricemultiplier", PriceMultiplier), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index 192065b94..af68c1464 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -131,18 +131,27 @@ namespace Barotrauma }; Locations[locationIndices.X].Connections.Add(connection); Locations[locationIndices.Y].Connections.Add(connection); - connection.LevelData = new LevelData(subElement.Element("Level")); string biomeId = subElement.GetAttributeString("biome", ""); connection.Biome = Biome.Prefabs.FirstOrDefault(b => b.Identifier == biomeId) ?? Biome.Prefabs.FirstOrDefault(b => !b.OldIdentifier.IsEmpty && b.OldIdentifier == biomeId) ?? Biome.Prefabs.First(); + connection.Difficulty = MathHelper.Clamp(connection.Difficulty, connection.Biome.MinDifficulty, connection.Biome.MaxDifficulty); + connection.LevelData = new LevelData(subElement.Element("Level"), connection.Difficulty); Connections.Add(connection); connectionElements.Add(subElement); break; } } + //backwards compatibility: location biomes weren't saved (or used for anything) previously, + //assign them if they haven't been assigned + Random rand = new MTRandom(ToolBox.StringToInt(Seed)); + if (Locations.First().Biome == null) + { + AssignBiomes(rand); + } + int startLocationindex = element.GetAttributeInt("startlocation", -1); if (startLocationindex > 0 && startLocationindex < Locations.Count) { @@ -237,6 +246,10 @@ namespace Barotrauma } } System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation."); + if (StartLocation?.LevelData != null) + { + StartLocation.LevelData.Difficulty = 0; + } //ensure all paths from the starting location have 0 difficulty to make the 1st campaign round very easy foreach (var locationConnection in StartLocation.Connections) @@ -251,6 +264,11 @@ namespace Barotrauma CurrentLocation.Discover(true); CurrentLocation.CreateStores(); + foreach (var location in Locations) + { + location.UnlockInitialMissions(); + } + InitProjectSpecific(); } @@ -505,22 +523,31 @@ namespace Barotrauma //remove orphans Locations.RemoveAll(l => !Connections.Any(c => c.Locations.Contains(l))); + AssignBiomes(new MTRandom(ToolBox.StringToInt(Seed))); + foreach (LocationConnection connection in Connections) { - //float difficulty = GetLevelDifficulty(connection.CenterPos.X / Width); - //connection.Difficulty = MathHelper.Clamp(difficulty + Rand.Range(-10.0f, 0.0f, Rand.RandSync.ServerAndClient), 1.2f, 100.0f); float difficulty = connection.CenterPos.X / Width * 100; - float random = difficulty > 10 ? 5 : 0; - connection.Difficulty = MathHelper.Clamp(difficulty + Rand.Range(-random, random, Rand.RandSync.ServerAndClient), 1.0f, 100.0f); + float minDifficulty = 0; + float maxDifficulty = 100; + var biome = connection.Biome; + if (biome != null) + { + minDifficulty = connection.Biome.MinDifficulty; + maxDifficulty = connection.Biome.MaxDifficulty; + if (connection.Locked) + { + connection.Difficulty = maxDifficulty; + } + } + connection.Difficulty = MathHelper.Clamp(difficulty, minDifficulty, maxDifficulty); } - AssignBiomes(); CreateEndLocation(); foreach (Location location in Locations) { location.LevelData = new LevelData(location, MathHelper.Clamp(location.MapPosition.X / Width * 100, 0.0f, 100.0f)); - location.UnlockInitialMissions(); } foreach (LocationConnection connection in Connections) { @@ -549,7 +576,7 @@ namespace Barotrauma return Biome.Prefabs.FirstOrDefault(b => b.AllowedZones.Contains(zoneIndex)); } - private void AssignBiomes() + private void AssignBiomes(Random rand) { var biomes = Biome.Prefabs; float zoneWidth = Width / generationParams.DifficultyZones; @@ -565,7 +592,7 @@ namespace Barotrauma { if (location.MapPosition.X < zoneX) { - location.Biome = allowedBiomes[Rand.Range(0, allowedBiomes.Count, Rand.RandSync.ServerAndClient)]; + location.Biome = allowedBiomes[rand.Next() % allowedBiomes.Count]; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs index 5397af405..48cce8bfc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs @@ -34,23 +34,6 @@ namespace Barotrauma return prefab; } - private void Dispose(bool disposing) - { - if (!Disposed) - { - if (disposing) - { - Humans.Clear(); - } - } - - Disposed = true; - } - - public override void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + public override void Dispose() { } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 23a7d0e1f..6972ac705 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -109,14 +109,21 @@ namespace Barotrauma #endif } - + public enum Type + { + WayPoint, + SpawnPoint + } + public WayPoint(Rectangle newRect, Submarine submarine) - : this (MapEntityPrefab.FindByIdentifier("waypoint".ToIdentifier()), newRect, submarine) + : this (Type.WayPoint, newRect, submarine) { } - public WayPoint(MapEntityPrefab prefab, Rectangle newRect, Submarine submarine, ushort id = Entity.NullEntityID) - : base (prefab, submarine, id) + public WayPoint(Type type, Rectangle newRect, Submarine submarine, ushort id = Entity.NullEntityID) + : base (type is Type.WayPoint + ? CoreEntityPrefab.WayPointPrefab + : CoreEntityPrefab.SpawnPointPrefab, submarine, id) { rect = newRect; idCardTags = Array.Empty(); @@ -1010,7 +1017,7 @@ namespace Barotrauma Enum.TryParse(element.GetAttributeString("spawn", "Path"), out SpawnType spawnType); - WayPoint w = new WayPoint(MapEntityPrefab.FindByIdentifier((spawnType == SpawnType.Path ? "waypoint" : "spawnpoint").ToIdentifier()), rect, submarine, idRemap.GetOffsetId(element)) + WayPoint w = new WayPoint(spawnType == SpawnType.Path ? Type.WayPoint : Type.SpawnPoint, rect, submarine, idRemap.GetOffsetId(element)) { spawnType = spawnType }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs index b269b2c76..b999e8e54 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs @@ -107,7 +107,7 @@ namespace Barotrauma.Networking return -1; } - //FIXME workaround for crash when closing the server under .NET 6.0, not sure if this is the proper way to fix it but it prevents it from crashing the client. - Markus + // BUG workaround for crash when closing the server under .NET 6.0, not sure if this is the proper way to fix it but it prevents it from crashing the client. - Markus #if NET6_0 try { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs index 495b5fd86..e5bf41bca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs @@ -45,6 +45,12 @@ namespace Barotrauma bool matchingElementFound = false; foreach (var subElement in element.Elements()) { + if (replacementSubElement.Name.ToString().Equals("clear", StringComparison.OrdinalIgnoreCase)) + { + matchingElementFound = true; + elementsToRemove.AddRange(element.Elements()); + break; + } if (!subElement.Name.ToString().Equals(replacementSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)) { continue; } if (i == index) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index 5c0e19cfd..368cb94ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -161,6 +161,7 @@ namespace Barotrauma RadialDistortion = true, InventoryScale = 1.0f, LightMapScale = 1.0f, + VisibleLightLimit = 50, TextScale = 1.0f, HUDScale = 1.0f, Specularity = true, @@ -200,6 +201,7 @@ namespace Barotrauma public float HUDScale; public float InventoryScale; public float LightMapScale; + public int VisibleLightLimit; public float TextScale; public bool RadialDistortion; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs index f8d4a1d2d..c193ba8d7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs @@ -406,23 +406,14 @@ namespace Barotrauma } } - private void Dispose(bool disposing) + public void Dispose() { if (!Disposed) { - if (disposing) - { - TargetComponents.Clear(); - } + TargetComponents.Clear(); } Disposed = true; } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index 027279c9d..bbb727d01 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -396,29 +396,20 @@ namespace Barotrauma return 1; } - private void Dispose(bool disposing) + public override void Dispose() { if (!disposed) { - if (disposing) - { - Prefabs.Remove(this); + Prefabs.Remove(this); #if CLIENT - Sprite?.Remove(); - Sprite = null; - DecorativeSprites.ForEach(sprite => sprite.Remove()); - targetProperties.Clear(); + Sprite?.Remove(); + Sprite = null; + DecorativeSprites.ForEach(sprite => sprite.Remove()); + targetProperties.Clear(); #endif - } } disposed = true; } - - public override void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/NamedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/NamedEvent.cs index 9f0a2f711..3f80b5500 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/NamedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/NamedEvent.cs @@ -7,11 +7,6 @@ namespace Barotrauma { private readonly Dictionary> events = new Dictionary>(); - ~NamedEvent() - { - ReleaseUnmanagedResources(); - } - public void Register(Identifier identifier, Action action) { if (HasEvent(identifier)) @@ -53,15 +48,9 @@ namespace Barotrauma } } - private void ReleaseUnmanagedResources() + public void Dispose() { events.Clear(); } - - public void Dispose() - { - ReleaseUnmanagedResources(); - GC.SuppressFinalize(this); - } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs index 573e248ab..ea2b7c2e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs @@ -429,8 +429,8 @@ namespace Barotrauma.IO public class FileStream : System.IO.Stream { - private System.IO.FileStream innerStream; - private string fileName; + private readonly System.IO.FileStream innerStream; + private readonly string fileName; public FileStream(string fn, System.IO.FileStream stream) { @@ -496,9 +496,9 @@ namespace Barotrauma.IO innerStream.Flush(); } - protected override void Dispose(bool disposing) + protected override void Dispose(bool notCalledByFinalizer) { - innerStream.Dispose(); + if (notCalledByFinalizer) { innerStream.Dispose(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index 365924358..c73f40124 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -479,7 +479,7 @@ namespace Barotrauma { int read = 0; - // FIXME workaround for .NET6 causing save decompression to fail + // BUG workaround for .NET6 causing save decompression to fail #if NET6_0 for (int i = 0; i < amount; i++) { diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index adc356451..ce02ef8f7 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,57 @@ +--------------------------------------------------------------------------------------------------------- +v0.18.2.0 +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Fixed "submarine equality check failed" errors in non-campaign multiplayer game modes. +- Fixed occasional crashes when exiting the sub editor. +- Fixed diving suit lights being on when not worn. +- Fixed research stations not working. +- Adjusted the layout of server settings gameplay tab to prevent overlaps on small resolutions. +- Fixed ignore orders not being loaded correctly in singleplayer. + +Changes: +- Lighting optimization: now some unimportant (dim and small) lights are hidden when there's lots of light sources visible on the screen at the same time. The maximum number of visible lights can be adjusted in the game settings. +- Lighting optimization: the number of light recalculations per frame is limited, meaning that when there's lots of moving, shadow-casting lights visible, the game doesn't try to recalculate the shadows all at the same time. +- Lighting optimization: simplify the light rendering when zoomed very far out (e.g. when looking through a periscope). +- Optimized status effects that modify items' conditions every frame (for example, oxygen tank shelves that fill up oxygen tanks). +- Hide AppData path from tooltips in the sub editor to prevent exposing the user's name. +- Reduce nausea chance of energy drink to 25%. +- Changes to the campaign progression in general. +- Changes to the level generation parameters, especially in Cold Caverns and the Ridge. +- Changes to the level resources distribution. +- Changes to the event manager settings (that affect the monster spawns). +- Adjusted and normalized the item loadouts for all the jobs. +- Changes to the items that always spawn with the sub at the beginning of the game (start items). +- Adjustments to the preferred containers (= where things are spawned and where they should be placed). +- Changes to the existing missions and how they are distributed. Added new missions. +- Reduced the costs for unlocking the biomes. +- Minor adjustments to the monster spawns. +- Changes to the item "gating". Some items don't appear early in the game anymore. +- Adjustments to the mission specific variants of the monsters. +- Added a large Crawler variant for some missions (removed the Swarmcrawler that was used for crawler missions). +- Halved Mudraptors' priority for eating dead bodies. + +Fixes: +- Fixed abyss area being very small in the Aphotic Plateau, preventing the abyss monster from reaching you if you go deep enough. +- Fixed status monitor displaying small amounts of water as 1% even though water detectors output 0%. +- Fixed autopilot conflicting with VELOCITY_IN inputs (now signals override the autopilot for 1 second). +- Fixed ConversationAction getting interrupted when opening an input-blocking menu in single player. +- Fixed sprite bleed in chaingun ammunition boxes. +- Fixed appearance of specific named NPCs being inconsistent (e.g. Captain Hognose sometimes being a woman or not having an eyepatch). +- Fixed certain scripted events getting stuck if you switch characters in single player (e.g. the events that require you to interact with fliers on the wall). +- Fixed crashing when the source of a rope is removed (e.g. when a latcher despawns while latched on to the sub). +- Fixed votes always going through if no-one votes. +- Fixed energy drink giving x10 more haste when used via the health interface. +- Fixed the monster spawns for the new game plus not working (currently a placeholder set). +- Fixed monsters spawning from missions not avoiding the engines. + +Modding: +- Level object, cave and mineral commonness can be defined based on the biome instead of the level generation parameters (= no need to define commonness for "coldcavernsbasic", "coldcavernsmaze" etc separately). +- Option to define ConversationAction texts directly in the event xml (instead of having to always define them in a spearate text file). +- Extended CustomInterface functionality with NumberInput elements that allow using float values ("numbertype") and defining the increment size ("step") the number of decimal places ("decimalplaces"). (Thanks, mLuby!) +- Implemented element for removing all the child elements of an element in a variant file. + --------------------------------------------------------------------------------------------------------- v0.18.1.0 --------------------------------------------------------------------------------------------------------- From 64db1a6a4405d6777b761a4cb89847545d3268c7 Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Tue, 31 May 2022 23:13:05 +0900 Subject: [PATCH 04/14] Build 0.18.4.0 --- .../Characters/CharacterNetworking.cs | 2 + .../Characters/Health/CharacterHealth.cs | 3 +- .../ContentPackage/ModProject.cs | 11 +- .../ContentManagement/ModMerger.cs | 133 ++++ .../Transition/UgcTransition.cs | 9 +- .../ClientSource/GUI/ChatBox.cs | 10 +- .../ClientSource/GUI/ComponentStyle.cs | 9 +- .../ClientSource/GUI/CrewManagement.cs | 5 +- .../ClientSource/GUI/FileSelection.cs | 6 +- .../BarotraumaClient/ClientSource/GUI/GUI.cs | 20 +- .../ClientSource/GUI/GUIButton.cs | 9 +- .../ClientSource/GUI/GUIComponent.cs | 2 + .../ClientSource/GUI/GUIContextMenu.cs | 3 +- .../ClientSource/GUI/GUIDropDown.cs | 3 +- .../ClientSource/GUI/GUIListBox.cs | 104 ++- .../ClientSource/GUI/GUINumberInput.cs | 8 +- .../ClientSource/GUI/GUIPrefab.cs | 20 +- .../ClientSource/GUI/GUIScrollBar.cs | 3 +- .../ClientSource/GUI/GUIStyle.cs | 7 +- .../ClientSource/GUI/GUITextBox.cs | 6 + .../ClientSource/GUI/GUITickBox.cs | 19 +- .../ClientSource/GUI/HUDLayoutSettings.cs | 2 +- .../ClientSource/GUI/MedicalClinicUI.cs | 9 +- .../ClientSource/GUI/Store.cs | 13 +- .../ClientSource/GUI/SubmarineSelection.cs | 125 ++- .../ClientSource/GUI/TabMenu.cs | 10 +- .../ClientSource/GUI/UpgradeStore.cs | 24 +- .../ClientSource/GUI/VotingInterface.cs | 19 +- .../BarotraumaClient/ClientSource/GameMain.cs | 23 +- .../ClientSource/GameSession/CrewManager.cs | 2 +- .../GameModes/MultiPlayerCampaign.cs | 500 +++++++----- .../GameModes/SinglePlayerCampaign.cs | 21 +- .../ClientSource/Items/CharacterInventory.cs | 194 +---- .../Items/Components/ItemComponent.cs | 18 +- .../Items/Components/Machines/Fabricator.cs | 1 + .../Items/Components/Machines/MiniMap.cs | 7 +- .../Components/Machines/OutpostTerminal.cs | 2 +- .../Items/Components/Machines/Steering.cs | 4 +- .../ClientSource/Items/Components/Planter.cs | 2 - .../Items/Components/Repairable.cs | 2 +- .../Items/Components/Signal/Terminal.cs | 2 +- .../ClientSource/Items/Item.cs | 13 + .../ClientSource/Map/Map/Map.cs | 10 +- .../ClientSource/Map/SubmarineInfo.cs | 4 +- .../ClientSource/Map/SubmarinePreview.cs | 26 +- .../ClientSource/Networking/GameClient.cs | 48 +- .../Primitives/Peers/SteamP2PClientPeer.cs | 2 +- .../Primitives/Peers/SteamP2POwnerPeer.cs | 24 +- .../ClientSource/Networking/Voting.cs | 40 +- .../CampaignSetupUI/CampaignSetupUI.cs | 269 ++++++- .../MultiPlayerCampaignSetupUI.cs | 95 +-- .../SinglePlayerCampaignSetupUI.cs | 118 +-- .../ClientSource/Screens/CampaignUI.cs | 2 +- .../CharacterEditor/CharacterEditorScreen.cs | 10 +- .../Screens/CharacterEditor/Wizard.cs | 10 +- .../Screens/EventEditor/EventEditorScreen.cs | 6 +- .../ClientSource/Screens/LevelEditorScreen.cs | 50 +- .../ClientSource/Screens/MainMenuScreen.cs | 19 +- .../ClientSource/Screens/NetLobbyScreen.cs | 15 +- .../Screens/ParticleEditorScreen.cs | 5 +- .../ClientSource/Screens/ServerListScreen.cs | 2 +- .../Screens/SpriteEditorScreen.cs | 16 +- .../ClientSource/Screens/SubEditorScreen.cs | 664 +++++++++++----- .../Serialization/SerializableEntityEditor.cs | 1 + .../ClientSource/Settings/SettingsMenu.cs | 2 +- .../ClientSource/Sounds/SoundPlayer.cs | 10 +- .../ClientSource/Steam/Workshop.cs | 7 +- .../Immutable/ImmutableWorkshopMenu.cs | 10 +- .../WorkshopMenu/Mutable/InstalledTab.cs | 746 ++++++++++++++++++ .../Steam/WorkshopMenu/Mutable/ItemList.cs | 5 +- .../WorkshopMenu/Mutable/ModListPreset.cs | 256 ++++++ .../Mutable/MutableWorkshopMenu.cs | 516 +----------- .../ClientSource/Steam/WorkshopMenu/UiUtil.cs | 7 +- .../Steam/WorkshopMenu/WorkshopMenu.cs | 5 + .../BarotraumaClient/LinuxClient.csproj | 3 +- Barotrauma/BarotraumaClient/MacClient.csproj | 3 +- .../BarotraumaClient/WindowsClient.csproj | 3 +- .../BarotraumaServer/LinuxServer.csproj | 3 +- Barotrauma/BarotraumaServer/MacServer.csproj | 3 +- .../Characters/CharacterNetworking.cs | 1 + .../ServerSource/DebugConsole.cs | 3 +- .../GameModes/MultiPlayerCampaign.cs | 298 ++++--- .../Items/Components/Machines/Steering.cs | 1 + .../ServerSource/Networking/Client.cs | 2 +- .../Networking/FileTransfer/ModSender.cs | 2 +- .../ServerSource/Networking/GameServer.cs | 82 +- .../Peers/Server/SteamP2PServerPeer.cs | 23 +- .../ServerSource/Networking/ServerSettings.cs | 25 +- .../ServerSource/Networking/Voting.cs | 53 +- .../BarotraumaServer/WindowsServer.csproj | 3 +- .../Data/campaignsettings.xml | 26 + .../SharedSource/Characters/AI/AITarget.cs | 50 +- .../Characters/AI/EnemyAIController.cs | 33 +- .../Characters/AI/HumanAIController.cs | 18 +- .../Characters/AI/Objectives/AIObjective.cs | 2 +- .../AI/Objectives/AIObjectiveFixLeak.cs | 33 +- .../AI/Objectives/AIObjectiveIdle.cs | 20 +- .../AI/Objectives/AIObjectiveManager.cs | 2 +- .../AI/Objectives/AIObjectiveRepairItem.cs | 2 +- .../Characters/AI/SteeringManager.cs | 6 +- .../Animation/FishAnimController.cs | 3 +- .../Characters/Animation/Ragdoll.cs | 36 +- .../SharedSource/Characters/Character.cs | 45 +- .../Health/Afflictions/Affliction.cs | 8 +- .../Health/Afflictions/AfflictionPrefab.cs | 15 +- .../Characters/Health/CharacterHealth.cs | 21 +- .../SharedSource/Characters/HumanPrefab.cs | 6 - .../Params/Ragdoll/RagdollParams.cs | 74 +- .../AbilityConditionData.cs | 1 - .../AbilityConditionDataless.cs | 4 +- .../Talents/Abilities/CharacterAbility.cs | 3 - .../CharacterAbilityApplyStatusEffects.cs | 1 - ...racterAbilityApplyStatusEffectsToAllies.cs | 8 +- ...cterAbilityApplyStatusEffectsToAttacker.cs | 5 +- ...pplyStatusEffectsToLastOrderedCharacter.cs | 4 +- ...rAbilityApplyStatusEffectsToNearestAlly.cs | 1 - .../AbilityGroups/CharacterAbilityGroup.cs | 1 - .../CharacterAbilityGroupEffect.cs | 17 +- .../CharacterAbilityGroupInterval.cs | 17 +- .../Characters/Talents/CharacterTalent.cs | 6 +- .../ContentFile/ContentFile.cs | 32 +- .../ContentPackage/ContentPackage.cs | 89 ++- .../ContentPackageManager.cs | 4 +- .../ContentManagement/ContentXElement.cs | 2 +- .../SharedSource/DebugConsole.cs | 4 +- .../BarotraumaShared/SharedSource/Enums.cs | 44 +- .../Events/EventActions/MissionAction.cs | 2 +- .../Events/EventActions/MoneyAction.cs | 2 - .../Events/EventActions/SpawnAction.cs | 90 ++- .../SharedSource/Events/EventManager.cs | 10 +- .../SharedSource/Events/MonsterEvent.cs | 2 +- .../GameSession/AutoItemPlacer.cs | 22 +- .../GameSession/GameModes/CampaignMode.cs | 80 +- .../GameModes/CampaignModePresets.cs | 82 ++ .../GameSession/GameModes/CampaignSettings.cs | 114 +++ .../GameSession/GameModes/MissionMode.cs | 6 +- .../GameModes/MultiPlayerCampaign.cs | 92 ++- .../SharedSource/GameSession/GameSession.cs | 36 +- .../SharedSource/Items/CharacterInventory.cs | 12 - .../Items/Components/GeneticMaterial.cs | 15 +- .../Items/Components/Holdable/RepairTool.cs | 40 +- .../Items/Components/ItemContainer.cs | 27 +- .../Items/Components/Machines/Controller.cs | 2 +- .../Components/Machines/Deconstructor.cs | 32 +- .../Items/Components/Machines/Steering.cs | 4 +- .../SharedSource/Items/Components/Planter.cs | 26 +- .../Items/Components/Repairable.cs | 2 +- .../Items/Components/Signal/MotionSensor.cs | 32 +- .../Items/Components/TriggerComponent.cs | 111 ++- .../SharedSource/Items/Components/Turret.cs | 77 +- .../SharedSource/Items/Components/Wearable.cs | 36 +- .../SharedSource/Items/Item.cs | 87 +- .../SharedSource/Items/StartItemSet.cs | 6 + .../SharedSource/Map/ItemAssemblyPrefab.cs | 3 +- .../SharedSource/Map/Levels/Level.cs | 95 ++- .../SharedSource/Map/Levels/LevelData.cs | 2 +- .../SharedSource/Map/LinkedSubmarine.cs | 2 +- .../SharedSource/Map/Map/Map.cs | 166 ++-- .../Map/Outposts/BeaconStationInfo.cs | 55 ++ .../Map/Outposts/OutpostGenerator.cs | 70 +- .../SharedSource/Map/Submarine.cs | 9 +- .../SharedSource/Map/SubmarineInfo.cs | 58 +- .../Networking/ChildServerRelay.cs | 24 +- .../SharedSource/Networking/NetworkMember.cs | 2 + .../SharedSource/Networking/ServerSettings.cs | 16 +- .../Serialization/SerializableProperty.cs | 9 + .../SharedSource/Settings/GameSettings.cs | 4 + .../StatusEffects/DelayedEffect.cs | 8 +- .../StatusEffects/StatusEffect.cs | 60 +- .../SharedSource/Steam/Workshop.cs | 14 +- .../SharedSource/Utils/Option/Option.cs | 15 + .../SharedSource/Utils/Result.cs | 9 +- .../SharedSource/Utils/SafeIO.cs | 17 + .../SharedSource/Utils/SaveUtil.cs | 4 + Barotrauma/BarotraumaShared/changelog.txt | 84 ++ 175 files changed, 4916 insertions(+), 2393 deletions(-) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ModMerger.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs create mode 100644 Barotrauma/BarotraumaShared/Data/campaignsettings.xml create mode 100644 Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignModePresets.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index ddf2b6bc6..7fd2cfdc0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -551,6 +551,7 @@ namespace Barotrauma { bool hasOwner = inc.ReadBoolean(); int ownerId = hasOwner ? inc.ReadByte() : -1; + float humanPrefabHealthMultiplier = inc.ReadSingle(); int balance = inc.ReadInt32(); int rewardDistribution = inc.ReadRangedInteger(0, 100); byte teamID = inc.ReadByte(); @@ -573,6 +574,7 @@ namespace Barotrauma { character.MerchantIdentifier = inc.ReadIdentifier(); } + character.HumanPrefabHealthMultiplier = humanPrefabHealthMultiplier; character.Wallet.Balance = balance; character.Wallet.RewardDistribution = rewardDistribution; if (character.CampaignInteractionType != CampaignMode.InteractionType.None) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 91e852beb..5c563220d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -6,7 +6,6 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -1345,6 +1344,7 @@ namespace Barotrauma { UserData = item, DisabledColor = Color.White * 0.1f, + PlaySoundOnSelect = false, OnClicked = (btn, userdata) => { if (!(userdata is ItemPrefab itemPrefab)) { return false; } @@ -1352,6 +1352,7 @@ namespace Barotrauma if (item == null) { return false; } Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == selectedLimbIndex); item.ApplyTreatment(Character.Controlled, Character, targetLimb); + SoundPlayer.PlayUISound(GUISoundType.Select); return true; } }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs index 166979300..088fb2725 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs @@ -108,6 +108,15 @@ namespace Barotrauma } } + public void RemoveFile(File file) + { + if (HasFile(file)) + { + files.Remove(file); + DiscardHashAndInstallTime(); + } + } + public void DiscardHashAndInstallTime() { ExpectedHash = null; @@ -144,7 +153,7 @@ namespace Barotrauma => rootElement.Add(new XAttribute(name, value.ToString() ?? "")); addRootAttribute("name", Name); - addRootAttribute("modversion", ModVersion); + if (!ModVersion.IsNullOrEmpty()) { addRootAttribute("modversion", ModVersion); } addRootAttribute("corepackage", IsCore); if (SteamWorkshopId != 0) { addRootAttribute("steamworkshopid", SteamWorkshopId); } addRootAttribute("gameversion", GameMain.Version); diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ModMerger.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ModMerger.cs new file mode 100644 index 000000000..43fe28e83 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ModMerger.cs @@ -0,0 +1,133 @@ +#nullable enable +using System; +using System.Linq; +using Barotrauma.Steam; +using Barotrauma.IO; + +namespace Barotrauma +{ + public static class ModMerger + { + public static void AskMerge(ContentPackage[] mods) + { + ErrorIfNonLocal(mods); + + var msgBox = new GUIMessageBox(TextManager.Get("MergeModsHeader"), "", relativeSize: (0.5f, 0.8f), + buttons: new LocalizedString[] { TextManager.Get("ConfirmModMerge"), TextManager.Get("Cancel") }); + msgBox.Buttons[1].OnClicked = msgBox.Close; + + var desc = new GUITextBlock(new RectTransform((1.0f, 0.1f), msgBox.Content.RectTransform), TextManager.Get("MergeModsDesc")); + var modsList = new GUIListBox(new RectTransform((1.0f, 0.5f), msgBox.Content.RectTransform)) + { + OnSelected = (component, o) => false, + HoverCursor = CursorState.Default + }; + foreach (var mod in mods) + { + new GUITextBlock(new RectTransform((1.0f, 0.11f), modsList.Content.RectTransform), mod.Name) + { + CanBeFocused = false + }; + } + var footer = new GUITextBlock(new RectTransform((1.0f, 0.1f), msgBox.Content.RectTransform), TextManager.Get("MergeModsFooter")); + var resultName = new GUITextBox(new RectTransform((1.0f, 0.1f), msgBox.Content.RectTransform)) + { + Text = (mods.Count(m => m.Files.Length > 1)==1) + ? mods.First(m => m.Files.Length > 1).Name + : "" + }; + + void flashText() + { + resultName!.Select(); + resultName.Flash(GUIStyle.Red); + } + + msgBox.Buttons[0].OnClicked = (button, o) => + { + if (string.IsNullOrEmpty(resultName.Text)) + { + flashText(); + return false; + } + string targetDir = $"{ContentPackage.LocalModsDir}/{resultName.Text}"; + + bool dirMatches(ContentPackage mod) + => mod.Dir.CleanUpPathCrossPlatform(correctFilenameCase: false) + .Equals(targetDir, StringComparison.OrdinalIgnoreCase); + if (ContentPackageManager.LocalPackages.Any(dirMatches) + && !mods.Any(dirMatches)) + { + flashText(); + return false; + } + + MergeMods(mods, resultName.Text); + msgBox.Close(); + return false; + }; + } + + private static void MergeMods(ContentPackage[] mods, string resultName) + { + ModProject resultProject = new ModProject + { + Name = resultName + }; + + string targetDir = $"{ContentPackage.LocalModsDir}/{resultName}"; + Directory.CreateDirectory(targetDir); + + foreach (var mod in mods) + { + foreach (var file in Directory.GetFiles(mod.Dir, "*", System.IO.SearchOption.AllDirectories) + .Select(f => f.CleanUpPathCrossPlatform(correctFilenameCase: false))) + { + if (Path.GetFileName(file).Equals(ContentPackage.FileListFileName, StringComparison.OrdinalIgnoreCase)) { continue; } + + string targetFilePath = file[mod.Dir.Length..]; + if (targetFilePath.StartsWith("/") || targetFilePath.StartsWith("\\")) + { + targetFilePath = targetFilePath[1..]; + } + + targetFilePath = Path.Combine(targetDir, targetFilePath).CleanUpPathCrossPlatform(correctFilenameCase: false); + //DebugConsole.NewMessage(targetFilePath); + + Directory.CreateDirectory(Path.GetDirectoryName(targetFilePath)!); + File.Copy(file, targetFilePath, overwrite: true); + + var oldFileInProject = resultProject.Files.FirstOrDefault(f + => f.Path.Equals(targetFilePath, StringComparison.OrdinalIgnoreCase)); + if (oldFileInProject != null) + { + resultProject.RemoveFile(oldFileInProject); + } + + var fileInMod = mod.Files.Find(f => f.Path == file); + if (fileInMod != null) + { + var newFileInProject = ModProject.File.FromPath(targetFilePath, fileInMod.GetType()); + resultProject.AddFile(newFileInProject); + } + } + } + resultProject.Save(Path.Combine(targetDir, ContentPackage.FileListFileName)); + + foreach (var mod in mods) + { + Directory.Delete(mod.Dir); + } + (SettingsMenu.Instance!.WorkshopMenu as MutableWorkshopMenu)!.PopulateInstalledModLists(forceRefreshEnabled: true, refreshDisabled: true); + } + + private static void ErrorIfNonLocal(ContentPackage[] mods) + { + var nonLocal = mods.Where(m => !ContentPackageManager.LocalPackages.Contains(m)).ToArray(); + if (nonLocal.Any()) + { + throw new Exception($"{string.Join(", ", nonLocal.Select(m => m.Name))} are not local mods"); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs index 795b4ae24..748995b91 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs @@ -1,7 +1,6 @@ #nullable enable using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -9,9 +8,7 @@ using System.Xml.Linq; using Barotrauma.Extensions; using Barotrauma.Steam; using Microsoft.Xna.Framework; -using Directory = Barotrauma.IO.Directory; -using File = Barotrauma.IO.File; -using Path = Barotrauma.IO.Path; +using Barotrauma.IO; namespace Barotrauma.Transition { @@ -258,13 +255,13 @@ namespace Barotrauma.Transition { string[] getFiles(string path, string pattern) => Directory.Exists(path) - ? Directory.GetFiles(path, pattern, SearchOption.TopDirectoryOnly) + ? Directory.GetFiles(path, pattern, System.IO.SearchOption.TopDirectoryOnly) : Array.Empty(); subs = getFiles(oldSubsPath, "*.sub"); itemAssemblies = getFiles(oldItemAssembliesPath, "*.xml"); - string[] allOldMods = Directory.GetDirectories(oldModsPath, "*", SearchOption.TopDirectoryOnly); + string[] allOldMods = Directory.GetDirectories(oldModsPath, "*", System.IO.SearchOption.TopDirectoryOnly); var publishedItems = await SteamManager.Workshop.GetPublishedItems(); foreach (var modDir in allOldMods) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index c1ee89fb8..1a780c3ec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -107,6 +107,7 @@ namespace Barotrauma var buttonLeft = new GUIButton(new RectTransform(new Vector2(0.1f, 0.8f), channelSettingsContent.RectTransform), style: "DeviceButton") { + PlaySoundOnSelect = false, OnClicked = (btn, userdata) => { if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) @@ -150,6 +151,7 @@ namespace Barotrauma var buttonRight = new GUIButton(new RectTransform(new Vector2(0.1f, 0.8f), channelSettingsContent.RectTransform), style: "DeviceButton") { + PlaySoundOnSelect = false, OnClicked = (btn, userdata) => { if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) @@ -178,6 +180,7 @@ namespace Barotrauma TextColor = new Color(51, 59, 46), SelectedTextColor = GUIStyle.Green, UserData = i, + PlaySoundOnSelect = false, OnClicked = (btn, userdata) => { if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) @@ -357,10 +360,15 @@ namespace Barotrauma CanBeFocused = true, ForceUpperCase = ForceUpperCase.No, UserData = message.SenderClient, + PlaySoundOnSelect = false, OnClicked = (_, o) => { if (!(o is Client client)) { return false; } - GameMain.NetLobbyScreen?.SelectPlayer(client); + if (GameMain.NetLobbyScreen != null) + { + GameMain.NetLobbyScreen.SelectPlayer(client); + SoundPlayer.PlayUISound(GUISoundType.Select); + } return true; }, OnSecondaryClicked = (_, o) => diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs index e99e15745..d5707afa4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs @@ -178,7 +178,14 @@ namespace Barotrauma return Sprites.ContainsKey(state) ? Sprites[state]?.First()?.Sprite : null; } - public void GetSize(XElement element) + public void RefreshSize() + { + Width = null; + Height = null; + GetSize(Element); + } + + private void GetSize(XElement element) { Point size = new Point(0, 0); foreach (var subElement in element.Elements()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index 43b772d1c..3aa3511df 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -193,12 +193,13 @@ namespace Barotrauma }; validateHiresButton = new GUIButton(new RectTransform(new Vector2(1.0f / 3.0f, 1.0f), group.RectTransform), text: TextManager.Get("campaigncrew.validate")) { - ClickSound = GUISoundType.HireRepairClick, + ClickSound = GUISoundType.ConfirmTransaction, ForceUpperCase = ForceUpperCase.Yes, OnClicked = (b, o) => ValidateHires(PendingHires, true) }; clearAllButton = new GUIButton(new RectTransform(new Vector2(1.0f / 3.0f, 1.0f), group.RectTransform), text: TextManager.Get("campaignstore.clearall")) { + ClickSound = GUISoundType.Cart, ForceUpperCase = ForceUpperCase.Yes, Enabled = HasPermission, OnClicked = (b, o) => RemoveAllPendingHires() @@ -403,6 +404,7 @@ namespace Barotrauma { var hireButton = new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementAddButton") { + ClickSound = GUISoundType.Cart, UserData = characterInfo, Enabled = HasPermission, OnClicked = (b, o) => AddPendingHire(o as CharacterInfo) @@ -429,6 +431,7 @@ namespace Barotrauma { new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementRemoveButton") { + ClickSound = GUISoundType.Cart, UserData = characterInfo, Enabled = HasPermission, OnClicked = (b, o) => RemovePendingHire(o as CharacterInfo) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs index 71023ab04..4fcd29809 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs @@ -182,7 +182,10 @@ namespace Barotrauma window = new GUIFrame(new RectTransform(Vector2.One * 0.8f, backgroundFrame.RectTransform, Anchor.Center)); var horizontalLayout = new GUILayoutGroup(new RectTransform(Vector2.One * 0.9f, window.RectTransform, Anchor.Center), true); - sidebar = new GUIListBox(new RectTransform(new Vector2(0.29f, 1.0f), horizontalLayout.RectTransform)); + sidebar = new GUIListBox(new RectTransform(new Vector2(0.29f, 1.0f), horizontalLayout.RectTransform)) + { + PlaySoundOnSelect = true + }; var drives = System.IO.DriveInfo.GetDrives(); foreach (var drive in drives) @@ -241,6 +244,7 @@ namespace Barotrauma fileList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.85f), fileListLayout.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = (child, userdata) => { if (userdata is null) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index ddddc7fc9..251799b66 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -24,15 +24,17 @@ namespace Barotrauma ChatMessage, RadioMessage, DeadMessage, - Click, + Select, PickItem, PickItemFail, DropItem, PopupMenu, - DecreaseQuantity, - IncreaseQuantity, - HireRepairClick, - UISwitch + Decrease, + Increase, + UISwitch, + TickBox, + ConfirmTransaction, + Cart, } public enum CursorState @@ -2384,7 +2386,7 @@ namespace Barotrauma CreateButton("PauseMenuResume", buttonContainer, null); CreateButton("PauseMenuSettings", buttonContainer, () => SettingsMenuOpen = true); - bool IsOutpostLevel() => GameMain.GameSession != null && Level.IsLoadedOutpost; + bool IsFriendlyOutpostLevel() => GameMain.GameSession != null && Level.IsLoadedFriendlyOutpost; if (Screen.Selected == GameMain.GameScreen && GameMain.GameSession != null) { if (GameMain.GameSession.GameMode is SinglePlayerCampaign spMode) @@ -2399,11 +2401,11 @@ namespace Barotrauma GameMain.GameSession.LoadPreviousSave(); }); - if (IsOutpostLevel()) + if (IsFriendlyOutpostLevel()) { CreateButton("PauseMenuSaveQuit", buttonContainer, verificationTextTag: "PauseMenuSaveAndReturnToMainMenuVerification", action: () => { - if (IsOutpostLevel()) { GameMain.QuitToMainMenu(save: true); } + if (IsFriendlyOutpostLevel()) { GameMain.QuitToMainMenu(save: true); } }); } } @@ -2416,7 +2418,7 @@ namespace Barotrauma } else if (!GameMain.GameSession.GameMode.IsSinglePlayer && GameMain.Client != null && GameMain.Client.HasPermission(ClientPermissions.ManageRound)) { - bool canSave = GameMain.GameSession.GameMode is CampaignMode && IsOutpostLevel(); + bool canSave = GameMain.GameSession.GameMode is CampaignMode && IsFriendlyOutpostLevel(); if (canSave) { CreateButton("PauseMenuSaveQuit", buttonContainer, verificationTextTag: "PauseMenuSaveAndReturnToServerLobbyVerification", action: () => diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs index 1846bc809..09e8107b1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs @@ -159,7 +159,9 @@ namespace Barotrauma private float pulseExpand; private bool flashed; - public GUISoundType ClickSound { get; set; } = GUISoundType.Click; + public GUISoundType ClickSound { get; set; } = GUISoundType.Select; + + public override bool PlaySoundOnSelect { get; set; } = true; public GUIButton(RectTransform rectT, Alignment textAlignment = Alignment.Center, string style = "", Color? color = null) : this(rectT, new RawLString(""), textAlignment, style, color) { } @@ -247,7 +249,10 @@ namespace Barotrauma } else if (PlayerInput.PrimaryMouseButtonClicked()) { - SoundPlayer.PlayUISound(ClickSound); + if (PlaySoundOnSelect) + { + SoundPlayer.PlayUISound(ClickSound); + } if (OnClicked != null) { if (OnClicked(this, UserData)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index deb5dab1c..dff86c500 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -383,6 +383,8 @@ namespace Barotrauma public bool ExternalHighlight = false; + public virtual bool PlaySoundOnSelect { get; set; } = false; + private RectTransform rectTransform; public RectTransform RectTransform { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs index 8b8e09f16..40eae2afa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs @@ -113,7 +113,8 @@ namespace Barotrauma { AutoHideScrollBar = false, ScrollBarVisible = false, - Padding = hasHeader ? new Vector4(4, 0, 4, 4) : padding + Padding = hasHeader ? new Vector4(4, 0, 4, 4) : padding, + PlaySoundOnSelect = true }; foreach (var (option, size) in optionsAndSizes) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index d34f6bf91..389456b9a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -183,7 +183,8 @@ namespace Barotrauma listBox = new GUIListBox(new RectTransform(new Point(Rect.Width, Rect.Height * MathHelper.Clamp(elementCount, 2, 10)), rectT, listAnchor, listPivot) { IsFixedSize = false }, style: null) { - Enabled = !selectMultiple + Enabled = !selectMultiple, + PlaySoundOnSelect = true, }; if (!selectMultiple) { listBox.OnSelected = SelectItem; } GUIStyle.Apply(listBox, "GUIListBox", this); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index 0089c3e94..8e43f9c6f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -309,6 +309,45 @@ namespace Barotrauma } } + public override bool PlaySoundOnSelect { get; set; } = false; + + public bool PlaySoundOnDragStop { get; set; } = false; + + public GUISoundType? SoundOnDragStart { get; set; } = null; + + public GUISoundType? SoundOnDragStop { get; set; } = null; + + #region enums + public enum Force + { + Yes, + No + } + + public enum AutoScroll + { + Enabled, + Disabled + } + + public enum TakeKeyBoardFocus + { + Yes, + No + } + + public enum PlaySelectSound + { + Yes, + No + } + + private AutoScroll GetAutoScroll(bool b) + { + return b ? AutoScroll.Enabled : AutoScroll.Disabled; + } + #endregion + /// For horizontal listbox, default side is on the bottom. For vertical, it's on the right. public GUIListBox(RectTransform rectT, bool isHorizontal = false, Color? color = null, string style = "", bool isScrollBarOnDefaultSide = true, bool useMouseDownToSelect = false) : base(style, rectT) { @@ -396,7 +435,7 @@ namespace Barotrauma UpdateScrollBarSize(); } - public void Select(object userData, bool force = false, bool autoScroll = true) + public void Select(object userData, Force force = Force.No, AutoScroll autoScroll = AutoScroll.Enabled) { var children = Content.Children; int i = 0; @@ -515,9 +554,12 @@ namespace Barotrauma /// Scrolls the list to the specific element. /// /// - public void ScrollToElement(GUIComponent component, bool playSound = true) + public void ScrollToElement(GUIComponent component, PlaySelectSound playSelectSound = PlaySelectSound.No) { - if (playSound) { SoundPlayer.PlayUISound(GUISoundType.Click); } + if (playSelectSound == PlaySelectSound.Yes) + { + SoundPlayer.PlayUISound(GUISoundType.Select); + } List children = Content.Children.ToList(); int index = children.IndexOf(component); if (index < 0) { return; } @@ -573,9 +615,16 @@ namespace Barotrauma } } + private double lastDragStartTime; + private void StartDraggingElement(GUIComponent child) { DraggedElement = child; + if (Timing.TotalTime > lastDragStartTime + 0.2f) + { + lastDragStartTime = Timing.TotalTime; + SoundPlayer.PlayUISound(SoundOnDragStart); + } } private bool UpdateDragging() @@ -586,6 +635,10 @@ namespace Barotrauma var draggedElem = draggedElement; OnRearranged?.Invoke(this, draggedElem.UserData); DraggedElement = null; + if (PlaySoundOnDragStop) + { + SoundPlayer.PlayUISound(SoundOnDragStop); + } RepositionChildren(); if (AllSelected.Contains(draggedElem)) { return true; } } @@ -710,7 +763,7 @@ namespace Barotrauma int index = Content.Children.ToList().IndexOf(component); if (index >= 0) { - Select(index, false, false, takeKeyBoardFocus: true); + Select(index, autoScroll: AutoScroll.Disabled, takeKeyBoardFocus: TakeKeyBoardFocus.Yes); } } } @@ -733,7 +786,7 @@ namespace Barotrauma { ScrollToElement(child); } - Select(i, autoScroll: false, takeKeyBoardFocus: true); + Select(i, autoScroll: AutoScroll.Disabled, takeKeyBoardFocus: TakeKeyBoardFocus.Yes, playSelectSound: PlaySelectSound.Yes); } if (CurrentDragMode != DragMode.NoDragging @@ -929,14 +982,13 @@ namespace Barotrauma if (ClampScrollToElements) { bool scrollDown = Math.Clamp(PlayerInput.ScrollWheelSpeed, 0, 1) > 0; - if (scrollDown) { - SelectPrevious(takeKeyBoardFocus: true); + SelectPrevious(takeKeyBoardFocus: TakeKeyBoardFocus.Yes, playSelectSound: PlaySelectSound.Yes); } else { - SelectNext(takeKeyBoardFocus: true); + SelectNext(takeKeyBoardFocus: TakeKeyBoardFocus.Yes, playSelectSound: PlaySelectSound.Yes); } } } @@ -964,7 +1016,7 @@ namespace Barotrauma return FindScrollableParentListBox(target.Parent); } - public void SelectNext(bool force = false, bool autoScroll = true, bool takeKeyBoardFocus = false) + public void SelectNext(Force force = Force.No, AutoScroll autoScroll = AutoScroll.Enabled, TakeKeyBoardFocus takeKeyBoardFocus = TakeKeyBoardFocus.No, PlaySelectSound playSelectSound = PlaySelectSound.No) { int index = SelectedIndex + 1; while (index < Content.CountChildren) @@ -972,10 +1024,10 @@ namespace Barotrauma GUIComponent child = Content.GetChild(index); if (child.Visible) { - Select(index, force, !SmoothScroll && autoScroll, takeKeyBoardFocus: takeKeyBoardFocus); + Select(index, force, GetAutoScroll(!SmoothScroll && autoScroll == AutoScroll.Enabled), takeKeyBoardFocus, playSelectSound); if (SmoothScroll) { - ScrollToElement(child); + ScrollToElement(child, playSelectSound); } break; } @@ -983,7 +1035,7 @@ namespace Barotrauma } } - public void SelectPrevious(bool force = false, bool autoScroll = true, bool takeKeyBoardFocus = false) + public void SelectPrevious(Force force = Force.No, AutoScroll autoScroll = AutoScroll.Enabled, TakeKeyBoardFocus takeKeyBoardFocus = TakeKeyBoardFocus.No, PlaySelectSound playSelectSound = PlaySelectSound.No) { int index = SelectedIndex - 1; while (index >= 0) @@ -991,10 +1043,10 @@ namespace Barotrauma GUIComponent child = Content.GetChild(index); if (child.Visible) { - Select(index, force, !SmoothScroll && autoScroll, takeKeyBoardFocus: takeKeyBoardFocus); + Select(index, force, GetAutoScroll(!SmoothScroll && autoScroll == AutoScroll.Enabled), takeKeyBoardFocus, playSelectSound); if (SmoothScroll) { - ScrollToElement(child); + ScrollToElement(child, playSelectSound); } break; } @@ -1002,7 +1054,7 @@ namespace Barotrauma } } - public void Select(int childIndex, bool force = false, bool autoScroll = true, bool takeKeyBoardFocus = false) + public void Select(int childIndex, Force force = Force.No, AutoScroll autoScroll = AutoScroll.Enabled, TakeKeyBoardFocus takeKeyBoardFocus = TakeKeyBoardFocus.No, PlaySelectSound playSelectSound = PlaySelectSound.No) { if (childIndex >= Content.CountChildren || childIndex < 0) { return; } @@ -1013,7 +1065,7 @@ namespace Barotrauma if (OnSelected != null) { // TODO: The callback is called twice, fix this! - wasSelected = force || OnSelected(child, child.UserData); + wasSelected = force == Force.Yes || OnSelected(child, child.UserData); } if (!wasSelected) { return; } @@ -1055,7 +1107,7 @@ namespace Barotrauma // Ensure that the selected element is visible. This may not be the case, if the selection is run from code. (e.g. if we have two list boxes that are synced) // TODO: This method only works when moving one item up/down (e.g. when using the up and down arrows) - if (autoScroll) + if (autoScroll == AutoScroll.Enabled) { if (ScrollBar.IsHorizontal) { @@ -1086,11 +1138,19 @@ namespace Barotrauma } // If one of the children is the subscriber, we don't want to register, because it will unregister the child. - if (takeKeyBoardFocus && CanTakeKeyBoardFocus && RectTransform.GetAllChildren().None(rt => rt.GUIComponent == GUI.KeyboardDispatcher.Subscriber)) + if (takeKeyBoardFocus == TakeKeyBoardFocus.Yes && CanTakeKeyBoardFocus && RectTransform.GetAllChildren().None(rt => rt.GUIComponent == GUI.KeyboardDispatcher.Subscriber)) { Selected = true; GUI.KeyboardDispatcher.Subscriber = this; } + + // List box child components can be parents to other components that can play sounds when selected (e.g. store elements) + // so the list box shouldn't play the Select sound if the GUI.MouseOn component has a sound to play + if (playSelectSound == PlaySelectSound.Yes && PlaySoundOnSelect && !child.PlaySoundOnSelect && + (GUI.MouseOn == null || GUI.MouseOn.Parent == Content || !GUI.MouseOn.PlaySoundOnSelect)) + { + SoundPlayer.PlayUISound(GUISoundType.Select); + } } public void Select(IEnumerable children) @@ -1293,16 +1353,16 @@ namespace Barotrauma switch (key) { case Keys.Down: - if (!isHorizontal && AllowArrowKeyScroll) { SelectNext(); } + if (!isHorizontal && AllowArrowKeyScroll) { SelectNext(playSelectSound: PlaySelectSound.Yes); } break; case Keys.Up: - if (!isHorizontal && AllowArrowKeyScroll) { SelectPrevious(); } + if (!isHorizontal && AllowArrowKeyScroll) { SelectPrevious(playSelectSound: PlaySelectSound.Yes); } break; case Keys.Left: - if (isHorizontal && AllowArrowKeyScroll) { SelectPrevious(); } + if (isHorizontal && AllowArrowKeyScroll) { SelectPrevious(playSelectSound: PlaySelectSound.Yes); } break; case Keys.Right: - if (isHorizontal && AllowArrowKeyScroll) { SelectNext(); } + if (isHorizontal && AllowArrowKeyScroll) { SelectNext(playSelectSound: PlaySelectSound.Yes); } break; case Keys.Enter: case Keys.Space: diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs index 7e049e601..f3802cca6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs @@ -182,7 +182,7 @@ namespace Barotrauma public float valueStep; private float pressedTimer; - private float pressedDelay = 0.5f; + 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) @@ -228,6 +228,7 @@ namespace Barotrauma 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); + PlusButton.ClickSound = GUISoundType.Increase; PlusButton.OnButtonDown += () => { pressedTimer = pressedDelay; @@ -249,6 +250,7 @@ namespace Barotrauma 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 += () => { pressedTimer = pressedDelay; @@ -423,8 +425,8 @@ namespace Barotrauma intValue = Math.Min(intValue, MaxValueInt.Value); UpdateText(); } - PlusButton.Enabled = intValue < MaxValueInt; - MinusButton.Enabled = intValue > MinValueInt; + PlusButton.Enabled = MaxValueInt == null || intValue < MaxValueInt; + MinusButton.Enabled = MinValueInt == null || intValue > MinValueInt; } private void UpdateText() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs index 1487d6943..137eee850 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs @@ -98,7 +98,6 @@ namespace Barotrauma foreach (var subElement in element.Elements().Reverse()) { if (subElement.NameAsIdentifier() != "override") { continue; } - if (subElement.GetAttributeBool("iscjk", false)) { return new ScalableFont(subElement, GameMain.Instance.GraphicsDevice); @@ -111,8 +110,7 @@ namespace Barotrauma { foreach (var subElement in element.Elements()) { - if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } - if (GameSettings.CurrentConfig.Language == subElement.GetAttributeIdentifier("language", "").ToLanguageIdentifier()) + if (IsValidOverride(subElement)) { return subElement.GetAttributeContentPath("file")?.Value; } @@ -125,8 +123,7 @@ namespace Barotrauma //check if any of the language override fonts want to override the font size as well foreach (var subElement in element.Elements()) { - if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } - if (GameSettings.CurrentConfig.Language == subElement.GetAttributeIdentifier("language", "").ToLanguageIdentifier()) + if (IsValidOverride(subElement)) { uint overrideFontSize = GetFontSize(subElement, 0); if (overrideFontSize > 0) { return (uint)Math.Round(overrideFontSize * GameSettings.CurrentConfig.Graphics.TextScale); } @@ -149,8 +146,7 @@ namespace Barotrauma { foreach (var subElement in element.Elements()) { - if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } - if (GameSettings.CurrentConfig.Language == subElement.GetAttributeIdentifier("language", "").ToLanguageIdentifier()) + if (IsValidOverride(subElement)) { return subElement.GetAttributeBool("dynamicloading", false); } @@ -162,14 +158,20 @@ namespace Barotrauma { foreach (var subElement in element.Elements()) { - if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } - if (GameSettings.CurrentConfig.Language == subElement.GetAttributeIdentifier("language", "").ToLanguageIdentifier()) + if (IsValidOverride(subElement)) { return subElement.GetAttributeBool("iscjk", false); } } return element.GetAttributeBool("iscjk", false); } + + private bool IsValidOverride(XElement element) + { + if (!element.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { return false; } + var languages = element.GetAttributeIdentifierArray("language", Array.Empty()); + return languages.Any(l => l.ToLanguageIdentifier() == GameSettings.CurrentConfig.Language); + } } public class GUIFont : GUISelector diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs index 1a17d1124..30ec4af6c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs @@ -322,9 +322,8 @@ namespace Barotrauma { if (!enabled || !PlayerInput.PrimaryMouseButtonDown()) { return false; } if (barSize >= 1.0f) { return false; } - DraggingBar = this; - + SoundPlayer.PlayUISound(GUISoundType.Select); return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index 850f347bb..d81ee4206 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -34,7 +34,6 @@ namespace Barotrauma public readonly static PrefabCollection ComponentStyles = new PrefabCollection(); public readonly static GUIFont Font = new GUIFont("Font"); - public readonly static GUIFont GlobalFont = new GUIFont("GlobalFont"); public readonly static GUIFont UnscaledSmallFont = new GUIFont("UnscaledSmallFont"); public readonly static GUIFont SmallFont = new GUIFont("SmallFont"); public readonly static GUIFont LargeFont = new GUIFont("LargeFont"); @@ -142,10 +141,6 @@ namespace Barotrauma public readonly static GUIColor HealthBarColorMedium = new GUIColor("HealthBarColorMedium"); public readonly static GUIColor HealthBarColorHigh = new GUIColor("HealthBarColorHigh"); - public readonly static GUIColor EquipmentIndicatorNotEquipped = new GUIColor("EquipmentIndicatorNotEquipped"); - public readonly static GUIColor EquipmentIndicatorEquipped = new GUIColor("EquipmentIndicatorEquipped"); - public readonly static GUIColor EquipmentIndicatorRunningOut = new GUIColor("EquipmentIndicatorRunningOut"); - public static Point ItemFrameMargin => new Point(50, 56).Multiply(GUI.SlicedSpriteScale); public static Point ItemFrameOffset => new Point(0, 3).Multiply(GUI.SlicedSpriteScale); @@ -159,7 +154,7 @@ namespace Barotrauma public static void Apply(GUIComponent targetComponent, Identifier styleName, GUIComponent parent = null) { - GUIComponentStyle componentStyle = null; + GUIComponentStyle componentStyle; if (parent != null) { GUIComponentStyle parentStyle = parent.Style; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index d48669710..fcbf3a5f4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -251,6 +251,8 @@ namespace Barotrauma public bool Readonly { get; set; } + public override bool PlaySoundOnSelect { get; set; } = true; + public GUITextBox(RectTransform rectT, string text = "", Color? textColor = null, GUIFont font = null, Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null, bool createClearButton = false, bool createPenIcon = true) : base(style, rectT) @@ -363,6 +365,10 @@ namespace Barotrauma selected = true; GUI.KeyboardDispatcher.Subscriber = this; OnSelected?.Invoke(this, Keys.None); + if (PlaySoundOnSelect) + { + SoundPlayer.PlayUISound(GUISoundType.Select); + } } public void Deselect() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs index 05e59d5fc..47ce9cab1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs @@ -1,15 +1,13 @@ using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; using System; -using System.Collections.Generic; namespace Barotrauma { public class GUITickBox : GUIComponent { - private GUILayoutGroup layoutGroup; - private GUIFrame box; - private GUITextBlock text; + private readonly GUILayoutGroup layoutGroup; + private readonly GUIFrame box; + private readonly GUITextBlock text; public delegate bool OnSelectedHandler(GUITickBox obj); public OnSelectedHandler OnSelected; @@ -129,6 +127,12 @@ namespace Barotrauma set { text.Text = value; } } + public float ContentWidth { get; private set; } + + public GUISoundType SoundType { private get; set; } = GUISoundType.TickBox; + + public override bool PlaySoundOnSelect { get; set; } = true; + public GUITickBox(RectTransform rectT, LocalizedString label, GUIFont font = null, string style = "") : base(null, rectT) { CanBeFocused = true; @@ -180,6 +184,7 @@ namespace Barotrauma box.RectTransform.MinSize = new Point(Rect.Height); box.RectTransform.Resize(box.RectTransform.MinSize); text.SetTextPos(); + ContentWidth = box.Rect.Width + text.Padding.X + text.TextSize.X + text.Padding.Z; } protected override void Update(float deltaTime) @@ -209,6 +214,10 @@ namespace Barotrauma { Selected = true; } + if (PlaySoundOnSelect) + { + SoundPlayer.PlayUISound(SoundType); + } } } else if (isSelected) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index ad3b9c4d8..f255c8f5c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -122,7 +122,7 @@ namespace Barotrauma //horizontal slices at the corners of the screen for health bar and affliction icons int afflictionAreaHeight = (int)(50 * GUI.Scale); - int healthBarWidth = (int)(BottomRightInfoArea.Width * 1.58f); + int healthBarWidth = (int)(BottomRightInfoArea.Width * 1.3f); int healthBarHeight = (int)(50f * GUI.Scale); HealthBarArea = new Rectangle(BottomRightInfoArea.Right - healthBarWidth + (int)Math.Floor(1 / GUI.Scale), BottomRightInfoArea.Y - healthBarHeight + GUI.IntScale(10), healthBarWidth, healthBarHeight); AfflictionAreaLeft = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index a86a2ca4c..54373bcff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -569,6 +569,7 @@ namespace Barotrauma GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), footerLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterRight); GUIButton healButton = new GUIButton(new RectTransform(new Vector2(0.33f, 1f), buttonLayout.RectTransform), TextManager.Get("medicalclinic.heal")) { + ClickSound = GUISoundType.ConfirmTransaction, Enabled = medicalClinic.PendingHeals.Any() && medicalClinic.GetBalance() >= medicalClinic.GetTotalCost(), OnClicked = (button, _) => { @@ -595,6 +596,7 @@ namespace Barotrauma GUIButton clearButton = new GUIButton(new RectTransform(new Vector2(0.33f, 1f), buttonLayout.RectTransform), TextManager.Get("campaignstore.clearall")) { + ClickSound = GUISoundType.Cart, OnClicked = (button, _) => { button.Enabled = false; @@ -684,6 +686,7 @@ namespace Barotrauma GUIButton healButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), textLayout.RectTransform), style: "CrewManagementRemoveButton") { + ClickSound = GUISoundType.Cart, OnClicked = (button, _) => { button.Enabled = false; @@ -766,6 +769,7 @@ namespace Barotrauma GUIButton treatAllButton = new GUIButton(new RectTransform(new Vector2(1f, 0.2f), mainLayout.RectTransform), TextManager.Get("medicalclinic.treatall")) { + ClickSound = GUISoundType.Cart, Font = GUIStyle.SubHeadingFont, Visible = false }; @@ -887,7 +891,10 @@ namespace Barotrauma GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.25f), bottomTextLayout.RectTransform), TextManager.FormatCurrency(affliction.Price), font: GUIStyle.SubHeadingFont); - GUIButton buyButton = new GUIButton(new RectTransform(new Vector2(0.2f, 0.75f), bottomLayout.RectTransform), style: "CrewManagementAddButton"); + GUIButton buyButton = new GUIButton(new RectTransform(new Vector2(0.2f, 0.75f), bottomLayout.RectTransform), style: "CrewManagementAddButton") + { + ClickSound = GUISoundType.Cart + }; ImmutableArray elementsToDisable = ImmutableArray.Create(prefabBlock, backgroundFrame, icon, vitalityBlock, severityBlock, buyButton, descriptionBlock, priceBlock); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index d54f1a9f5..105bd7a07 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -390,7 +390,7 @@ namespace Barotrauma ToolTip = TextManager.Get("campaignstore.reputationtooltip") }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), reputationEffectContainer.RectTransform), - TextManager.Get("reputation"), font: GUIStyle.Font, textAlignment: Alignment.BottomLeft) + TextManager.Get("reputationmodifier"), font: GUIStyle.Font, textAlignment: Alignment.BottomLeft) { AutoScaleVertical = true, CanBeFocused = false, @@ -656,7 +656,7 @@ namespace Barotrauma SetConfirmButtonBehavior(); clearAllButton = new GUIButton(new RectTransform(new Vector2(0.35f, 1.0f), buttonContainer.RectTransform), TextManager.Get("campaignstore.clearall")) { - ClickSound = GUISoundType.DecreaseQuantity, + ClickSound = GUISoundType.Cart, Enabled = HasActiveTabPermissions(), ForceUpperCase = ForceUpperCase.Yes, OnClicked = (button, userData) => @@ -1567,8 +1567,6 @@ namespace Barotrauma } AddToShoppingCrate(purchasedItem, quantity: numberInput.IntValue - purchasedItem.Quantity); }; - amountInput.PlusButton.ClickSound = GUISoundType.IncreaseQuantity; - amountInput.MinusButton.ClickSound = GUISoundType.DecreaseQuantity; frame.HoverColor = frame.SelectedColor = Color.Transparent; } @@ -1622,7 +1620,7 @@ namespace Barotrauma { new GUIButton(new RectTransform(new Vector2(buttonRelativeWidth, 0.9f), mainGroup.RectTransform), style: "StoreAddToCrateButton") { - ClickSound = GUISoundType.IncreaseQuantity, + ClickSound = GUISoundType.Cart, Enabled = !forceDisable && pi.Quantity > 0, ForceUpperCase = ForceUpperCase.Yes, UserData = "addbutton", @@ -1633,7 +1631,7 @@ namespace Barotrauma { new GUIButton(new RectTransform(new Vector2(buttonRelativeWidth, 0.9f), mainGroup.RectTransform), style: "StoreRemoveFromCrateButton") { - ClickSound = GUISoundType.DecreaseQuantity, + ClickSound = GUISoundType.Cart, Enabled = !forceDisable, ForceUpperCase = ForceUpperCase.Yes, UserData = "removebutton", @@ -2076,11 +2074,13 @@ namespace Barotrauma { if (IsBuying) { + confirmButton.ClickSound = GUISoundType.ConfirmTransaction; confirmButton.Text = TextManager.Get("CampaignStore.Purchase"); confirmButton.OnClicked = (b, o) => BuyItems(); } else { + confirmButton.ClickSound = GUISoundType.Select; confirmButton.Text = TextManager.Get("CampaignStoreTab.Sell"); confirmButton.OnClicked = (b, o) => { @@ -2088,6 +2088,7 @@ namespace Barotrauma TextManager.Get("FireWarningHeader"), TextManager.Get("CampaignStore.SellWarningText"), new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); + confirmDialog.Buttons[0].ClickSound = GUISoundType.ConfirmTransaction; confirmDialog.Buttons[0].OnClicked = (b, o) => SellItems(); confirmDialog.Buttons[0].OnClicked += confirmDialog.Close; confirmDialog.Buttons[1].OnClicked = confirmDialog.Close; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 707e3580a..28de3502a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -29,6 +29,8 @@ namespace Barotrauma private GUITextBlock descriptionTextBlock; private int selectionIndicatorThickness; private GUIImage listBackground; + private GUITickBox transferItemsTickBox; + private GUITextBlock itemTransferReminderBlock; private readonly List subsToShow; private readonly SubmarineDisplayContent[] submarineDisplays = new SubmarineDisplayContent[submarinesPerPage]; @@ -61,6 +63,23 @@ namespace Barotrauma public GUIButton previewButton; } + private bool TransferItemsOnSwitch + { + get + { + return transferItemsOnSwitch; + } + set + { + transferItemsOnSwitch = value; + if (transferItemsTickBox != null) + { + transferItemsTickBox.Selected = value; + } + } + } + private bool transferItemsOnSwitch = true; + public SubmarineSelection(bool transfer, Action closeAction, RectTransform parent) { if (GameMain.GameSession.Campaign == null) { return; } @@ -149,11 +168,12 @@ namespace Barotrauma GUIListBox descriptionFrame = new GUIListBox(new RectTransform(new Vector2(0.59f, 1f), infoFrame.RectTransform), style: null) { Padding = new Vector4(HUDLayoutSettings.Padding / 2f, HUDLayoutSettings.Padding * 1.5f, HUDLayoutSettings.Padding * 1.5f, HUDLayoutSettings.Padding / 2f) }; descriptionTextBlock = new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionFrame.Content.RectTransform), string.Empty, font: GUIStyle.Font, wrap: true) { CanBeFocused = false }; - GUILayoutGroup buttonFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.075f), content.RectTransform), childAnchor: Anchor.CenterRight) { IsHorizontal = true, AbsoluteSpacing = HUDLayoutSettings.Padding }; + GUILayoutGroup bottomContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.075f), content.RectTransform, Anchor.CenterRight), childAnchor: Anchor.CenterRight) { IsHorizontal = true, AbsoluteSpacing = HUDLayoutSettings.Padding }; + float transferInfoFrameWidth = 1.0f; if (closeAction != null) { - GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), buttonFrame.RectTransform), TextManager.Get("Close"), style: "GUIButtonFreeScale") + GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), bottomContainer.RectTransform), TextManager.Get("Close"), style: "GUIButtonFreeScale") { OnClicked = (button, userData) => { @@ -161,11 +181,33 @@ namespace Barotrauma return true; } }; + transferInfoFrameWidth -= closeButton.RectTransform.RelativeSize.X; } - if (purchaseService) confirmButtonAlt = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), buttonFrame.RectTransform), purchaseOnlyText, style: "GUIButtonFreeScale"); - confirmButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), buttonFrame.RectTransform), purchaseService ? purchaseAndSwitchText : deliveryFee > 0 ? deliveryText : switchText, style: "GUIButtonFreeScale"); + if (purchaseService) + { + confirmButtonAlt = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), bottomContainer.RectTransform), purchaseOnlyText, style: "GUIButtonFreeScale"); + transferInfoFrameWidth -= confirmButtonAlt.RectTransform.RelativeSize.X; + } + confirmButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), bottomContainer.RectTransform), purchaseService ? purchaseAndSwitchText : deliveryFee > 0 ? deliveryText : switchText, style: "GUIButtonFreeScale"); SetConfirmButtonState(false); + transferInfoFrameWidth -= confirmButton.RectTransform.RelativeSize.X; + GUIFrame transferInfoFrame = new GUIFrame(new RectTransform(new Vector2(transferInfoFrameWidth, 1.0f), bottomContainer.RectTransform), style: null) + { + CanBeFocused = false + }; + transferItemsTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 1.0f), transferInfoFrame.RectTransform, Anchor.CenterRight), TextManager.Get("transferitems"), font: GUIStyle.SubHeadingFont) + { + Selected = TransferItemsOnSwitch, + Visible = false, + OnSelected = (tb) => transferItemsOnSwitch = tb.Selected + }; + transferItemsTickBox.RectTransform.Resize(new Point(Math.Min((int)transferItemsTickBox.ContentWidth, transferInfoFrame.Rect.Width), transferItemsTickBox.Rect.Height)); + itemTransferReminderBlock = new GUITextBlock(new RectTransform(Vector2.One, transferInfoFrame.RectTransform, Anchor.CenterRight), null) + { + TextAlignment = Alignment.CenterRight, + Visible = false + }; pageIndicatorHolder = new GUIFrame(new RectTransform(new Vector2(1f, 1.5f), submarineControlsGroup.RectTransform), style: null); pageIndicator = GUIStyle.GetComponentStyle("GUIPageIndicator").GetDefaultSprite(); @@ -272,7 +314,7 @@ namespace Barotrauma } } - public void RefreshSubmarineDisplay(bool updateSubs) + public void RefreshSubmarineDisplay(bool updateSubs, bool setTransferOptionToTrue = false) { if (!initialized) { @@ -286,6 +328,10 @@ namespace Barotrauma { playerBalanceElement = CampaignUI.UpdateBalanceElement(playerBalanceElement); } + if (setTransferOptionToTrue) + { + TransferItemsOnSwitch = true; + } if (updateSubs) { UpdateSubmarines(); @@ -401,6 +447,10 @@ namespace Barotrauma { SelectSubmarine(null, Rectangle.Empty); } + else + { + UpdateItemTransferInfoFrame(); + } } private void UpdateSubmarines() @@ -553,6 +603,40 @@ namespace Barotrauma selectedSubmarineIndicator.RectTransform.NonScaledSize = Point.Zero; SetConfirmButtonState(false); } + + UpdateItemTransferInfoFrame(); + } + + private void UpdateItemTransferInfoFrame() + { + if (selectedSubmarine != null) + { + var pendingSub = GameMain.GameSession?.Campaign?.PendingSubmarineSwitch; + if (Submarine.MainSub?.Info?.Name == selectedSubmarine.Name && pendingSub == null) + { + transferItemsTickBox.Visible = false; + itemTransferReminderBlock.Visible = false; + } + else if (pendingSub?.Name == selectedSubmarine.Name) + { + transferItemsTickBox.Visible = false; + itemTransferReminderBlock.Text = GameMain.GameSession.Campaign.TransferItemsOnSubSwitch ? + TextManager.Get("itemtransferenabledreminder") : + TextManager.Get("itemtransferdisabledreminder"); + itemTransferReminderBlock.Visible = true; + } + else + { + transferItemsTickBox.Selected = TransferItemsOnSwitch; + transferItemsTickBox.Visible = true; + itemTransferReminderBlock.Visible = false; + } + } + else + { + transferItemsTickBox.Visible = false; + itemTransferReminderBlock.Visible = false; + } } private void SetConfirmButtonState(bool state) @@ -614,24 +698,27 @@ namespace Barotrauma ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName), ("[amount]", deliveryFee.ToString()), ("[currencyname]", currencyName)), messageBoxOptions); + msgBox.Buttons[0].ClickSound = GUISoundType.ConfirmTransaction; } else { - msgBox = new GUIMessageBox(TextManager.Get("switchsubmarineheader"), TextManager.GetWithVariables("switchsubmarinetext", + var text = TextManager.GetWithVariables("switchsubmarinetext", ("[submarinename1]", CurrentOrPendingSubmarine().DisplayName), - ("[submarinename2]", selectedSubmarine.DisplayName)), messageBoxOptions); + ("[submarinename2]", selectedSubmarine.DisplayName)); + text += GetItemTransferText(); + msgBox = new GUIMessageBox(TextManager.Get("switchsubmarineheader"), text, messageBoxOptions); } msgBox.Buttons[0].OnClicked = (applyButton, obj) => { if (GameMain.Client == null) { - GameMain.GameSession.SwitchSubmarine(selectedSubmarine, deliveryFee); + GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch, deliveryFee); RefreshSubmarineDisplay(true); } else { - GameMain.Client.InitiateSubmarineChange(selectedSubmarine, Networking.VoteType.SwitchSub); + GameMain.Client.InitiateSubmarineChange(selectedSubmarine, TransferItemsOnSwitch, Networking.VoteType.SwitchSub); } return true; }; @@ -653,23 +740,25 @@ namespace Barotrauma if (!purchaseOnly) { - msgBox = new GUIMessageBox(TextManager.Get("purchaseandswitchsubmarineheader"), TextManager.GetWithVariables("purchaseandswitchsubmarinetext", + var text = TextManager.GetWithVariables("purchaseandswitchsubmarinetext", ("[submarinename1]", selectedSubmarine.DisplayName), ("[amount]", selectedSubmarine.Price.ToString()), ("[currencyname]", currencyName), - ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName)), messageBoxOptions); + ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName)); + text += GetItemTransferText(); + msgBox = new GUIMessageBox(TextManager.Get("purchaseandswitchsubmarineheader"), text, messageBoxOptions); msgBox.Buttons[0].OnClicked = (applyButton, obj) => { if (GameMain.Client == null) { GameMain.GameSession.PurchaseSubmarine(selectedSubmarine); - GameMain.GameSession.SwitchSubmarine(selectedSubmarine, 0); + GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch, 0); RefreshSubmarineDisplay(true); } else { - GameMain.Client.InitiateSubmarineChange(selectedSubmarine, Networking.VoteType.PurchaseAndSwitchSub); + GameMain.Client.InitiateSubmarineChange(selectedSubmarine, TransferItemsOnSwitch, Networking.VoteType.PurchaseAndSwitchSub); } return true; }; @@ -690,14 +779,20 @@ namespace Barotrauma } else { - GameMain.Client.InitiateSubmarineChange(selectedSubmarine, Networking.VoteType.PurchaseSub); + GameMain.Client.InitiateSubmarineChange(selectedSubmarine, false, Networking.VoteType.PurchaseSub); } return true; }; } + msgBox.Buttons[0].ClickSound = GUISoundType.ConfirmTransaction; msgBox.Buttons[0].OnClicked += msgBox.Close; msgBox.Buttons[1].OnClicked = msgBox.Close; - } + } + + private LocalizedString GetItemTransferText() + { + return "\n\n" + TextManager.Get(TransferItemsOnSwitch ? "itemswillbetransferred" : "itemswontbetransferred"); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 8a71ba121..cbf116f0b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -360,7 +360,7 @@ namespace Barotrauma var talentsButton = createTabButton(InfoFrameTab.Talents, "tabmenu.character"); talentsButton.OnAddedToGUIUpdateList += (component) => { - talentsButton.Enabled = Character.Controlled?.Info != null || (GameMain.Client?.CharacterInfo != null && GameMain.GameSession?.GameMode is MultiPlayerCampaign); + talentsButton.Enabled = Character.Controlled?.Info != null || GameMain.Client?.CharacterInfo != null; if (!talentsButton.Enabled && selectedTab == InfoFrameTab.Talents) { SelectInfoFrameTab(InfoFrameTab.Crew); @@ -453,7 +453,8 @@ namespace Barotrauma GUIListBox crewList = new GUIListBox(new RectTransform(crewListSize, content.RectTransform)) { Padding = new Vector4(2, 5, 0, 0), - AutoHideScrollBar = false + AutoHideScrollBar = false, + PlaySoundOnSelect = true }; crewList.UpdateDimensions(); @@ -928,8 +929,8 @@ namespace Barotrauma } else { - Vector2 stringOffset = GUIStyle.GlobalFont.MeasureString(inLobbyString) / 2f; - GUIStyle.GlobalFont.DrawString(spriteBatch, inLobbyString, area.Center.ToVector2() - stringOffset, Color.White); + Vector2 stringOffset = GUIStyle.Font.MeasureString(inLobbyString) / 2f; + GUIStyle.Font.DrawString(spriteBatch, inLobbyString, area.Center.ToVector2() - stringOffset, Color.White); } } @@ -1914,6 +1915,7 @@ namespace Barotrauma { OnClicked = (button, o) => { + GameMain.Client?.SendCharacterInfo(); characterSettingsFrame!.Visible = false; talentFrameMain.Visible = true; return true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index f694cb4d5..6833b74d3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -462,7 +462,7 @@ namespace Barotrauma button.Enabled = false; } return true; - }); + }, overrideConfirmButtonSound: GUISoundType.ConfirmTransaction); } else { @@ -497,7 +497,7 @@ namespace Barotrauma button.Enabled = false; } return true; - }); + }, overrideConfirmButtonSound: GUISoundType.ConfirmTransaction); } else { @@ -539,7 +539,7 @@ namespace Barotrauma GameMain.Client?.SendCampaignState(); } return true; - }); + }, overrideConfirmButtonSound: GUISoundType.ConfirmTransaction); } else { @@ -589,7 +589,7 @@ namespace Barotrauma new GUITextBlock(rectT(1, 0, textLayout), title, font: GUIStyle.SubHeadingFont) { CanBeFocused = false, AutoScaleHorizontal = true }; new GUITextBlock(rectT(1, 0, textLayout), TextManager.FormatCurrency(price)); GUILayoutGroup buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, contentLayout), childAnchor: Anchor.Center) { UserData = "buybutton" }; - new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "RepairBuyButton") { ClickSound = GUISoundType.HireRepairClick, Enabled = PlayerBalance >= price && !isDisabled, OnClicked = onPressed }; + new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "RepairBuyButton") { Enabled = PlayerBalance >= price && !isDisabled, OnClicked = onPressed }; contentLayout.Recalculate(); buyButtonLayout.Recalculate(); @@ -622,7 +622,8 @@ namespace Barotrauma PadBottom = true, SelectTop = true, ClampScrollToElements = true, - Spacing = 8 + Spacing = 8, + PlaySoundOnSelect = true }; Dictionary> upgrades = new Dictionary>(); @@ -1123,7 +1124,10 @@ namespace Barotrauma { priceText.Text = string.Empty; } - new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: buttonStyle) { Enabled = false }; + new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: buttonStyle) + { + Enabled = false + }; if (upgradePrefab != null) { var increaseText = new GUITextBlock(rectT(1, 0.2f, buyButtonLayout), "", textAlignment: Alignment.Center); @@ -1212,7 +1216,7 @@ namespace Barotrauma Campaign.UpgradeManager.PurchaseUpgrade(prefab, category); GameMain.Client?.SendCampaignState(); return true; - }); + }, overrideConfirmButtonSound: GUISoundType.ConfirmTransaction); return true; }; @@ -1400,7 +1404,7 @@ namespace Barotrauma if (PlayerInput.PrimaryMouseButtonClicked() && selectedUpgradeTab == UpgradeTab.Upgrade && currentStoreLayout != null) { - ScrollToCategory(data => data.Category.IsWallUpgrade); + ScrollToCategory(data => data.Category.IsWallUpgrade, GUIListBox.PlaySelectSound.Yes); } } } @@ -1682,7 +1686,7 @@ namespace Barotrauma } } - private void ScrollToCategory(Predicate predicate) + private void ScrollToCategory(Predicate predicate, GUIListBox.PlaySelectSound playSelectSound = GUIListBox.PlaySelectSound.No) { if (currentStoreLayout == null) { return; } @@ -1690,7 +1694,7 @@ namespace Barotrauma { if (child.UserData is CategoryData data && predicate(data)) { - currentStoreLayout.ScrollToElement(child); + currentStoreLayout.ScrollToElement(child, playSelectSound); break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs index e54913559..7d83f6e99 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs @@ -26,7 +26,7 @@ namespace Barotrauma private Color SubmarineColor => GUIStyle.Orange; private Point createdForResolution; - public static VotingInterface CreateSubmarineVotingInterface(Client starter, SubmarineInfo info, VoteType type, float votingTime) + public static VotingInterface CreateSubmarineVotingInterface(Client starter, SubmarineInfo info, VoteType type, bool transferItems, float votingTime) { if (starter == null || info == null) { return null; } @@ -38,7 +38,7 @@ namespace Barotrauma getMaxVotes = () => GameMain.NetworkMember?.Voting?.GetVoteCountMax(type) ?? 0, }; subVoting.onVoteEnd = () => subVoting.SendSubmarineVoteEndMessage(info, type); - subVoting.SetSubmarineVotingText(starter, info, type); + subVoting.SetSubmarineVotingText(starter, info, transferItems, type); subVoting.Initialize(starter, type); return subVoting; } @@ -160,19 +160,21 @@ namespace Barotrauma } #region Submarine Voting - private void SetSubmarineVotingText(Client starter, SubmarineInfo info, VoteType type) + + private void SetSubmarineVotingText(Client starter, SubmarineInfo info, bool transferItems, VoteType type) { string name = starter.Name; JobPrefab prefab = starter?.Character?.Info?.Job?.Prefab; Color nameColor = prefab != null ? prefab.UIColor : Color.White; string characterRichString = $"‖color:{nameColor.R},{nameColor.G},{nameColor.B}‖{name}‖color:end‖"; string submarineRichString = $"‖color:{SubmarineColor.R},{SubmarineColor.G},{SubmarineColor.B}‖{info.DisplayName}‖color:end‖"; - + string tag = string.Empty; LocalizedString text = string.Empty; switch (type) { case VoteType.PurchaseAndSwitchSub: - text = TextManager.GetWithVariables("submarinepurchaseandswitchvote", + tag = transferItems ? "submarinepurchaseandswitchwithitemsvote" : "submarinepurchaseandswitchvote"; + text = TextManager.GetWithVariables(tag, ("[playername]", characterRichString), ("[submarinename]", submarineRichString), ("[amount]", info.Price.ToString()), @@ -189,7 +191,8 @@ namespace Barotrauma int deliveryFee = SubmarineSelection.DeliveryFeePerDistanceTravelled * GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation); if (deliveryFee > 0) { - text = TextManager.GetWithVariables("submarineswitchfeevote", + tag = transferItems ? "submarineswitchwithitemsfeevote" : "submarineswitchfeevote"; + text = TextManager.GetWithVariables(tag, ("[playername]", characterRichString), ("[submarinename]", submarineRichString), ("[locationname]", endLocation.Name), @@ -198,13 +201,13 @@ namespace Barotrauma } else { - text = TextManager.GetWithVariables("submarineswitchnofeevote", + tag = transferItems ? "submarineswitchwithitemsnofeevote" : "submarineswitchnofeevote"; + text = TextManager.GetWithVariables(tag, ("[playername]", characterRichString), ("[submarinename]", submarineRichString)); } break; } - votingOnText = RichString.Rich(text); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 188c91bed..01b319c25 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -943,6 +943,23 @@ namespace Barotrauma Timing.Accumulator = 0.0f; } + private void FixRazerCortex() + { +#if WINDOWS + //Razer Cortex's overlay is broken. + //For whatever reason, it messes up the blendstate and, + //because MonoGame reasonably assumes that you don't need + //to touch it if you're setting it to the exact same one + //you were already using, it doesn't fix Razer's mess. + //Therefore, we need to change the blendstate TWICE: + //once to force MonoGame to change it, and then again to + //use the blendstate we actually want. + var oldBlendState = GraphicsDevice.BlendState; + GraphicsDevice.BlendState = oldBlendState == BlendState.Opaque ? BlendState.NonPremultiplied : BlendState.Opaque; + GraphicsDevice.BlendState = oldBlendState; +#endif + } + /// /// This is called when the game should draw itself. /// @@ -950,7 +967,9 @@ namespace Barotrauma { Stopwatch sw = new Stopwatch(); sw.Start(); - + + FixRazerCortex(); + double deltaTime = gameTime.ElapsedGameTime.TotalSeconds; if (Timing.FrameLimit > 0) @@ -1043,7 +1062,7 @@ namespace Barotrauma } // Update store stock when saving and quitting in an outpost (normally updated when CampaignMode.End() is called) - if (GameSession?.Campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedOutpost && spCampaign.Map?.CurrentLocation != null && spCampaign.CargoManager != null) + if (GameSession?.Campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedFriendlyOutpost && spCampaign.Map?.CurrentLocation != null && spCampaign.CargoManager != null) { spCampaign.Map.CurrentLocation.AddStock(spCampaign.CargoManager.SoldItems); spCampaign.CargoManager.ClearSoldItemsProjSpecific(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 7dbeffd05..52fa7e3cd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -1608,7 +1608,7 @@ namespace Barotrauma { if (character == Character.Controlled && crewList.SelectedComponent != characterComponent) { - crewList.Select(character, force: true); + crewList.Select(character, GUIListBox.Force.Yes); } // Icon colors might change based on the target so we check if they need to be updated if (GetCurrentOrderIconList(characterComponent) is GUIListBox currentOrderIconList) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 0bde737f0..41d89b0d5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -587,196 +587,78 @@ namespace Barotrauma //static because we may need to instantiate the campaign if it hasn't been done yet public static void ClientRead(IReadMessage msg) { + NetFlags requiredFlags = (NetFlags)msg.ReadUInt16(); + bool isFirstRound = msg.ReadBoolean(); byte campaignID = msg.ReadByte(); - UInt16 updateID = msg.ReadUInt16(); UInt16 saveID = msg.ReadUInt16(); string mapSeed = msg.ReadString(); - UInt16 currentLocIndex = msg.ReadUInt16(); - UInt16 selectedLocIndex = msg.ReadUInt16(); - byte selectedMissionCount = msg.ReadByte(); - List selectedMissionIndices = new List(); - for (int i = 0; i < selectedMissionCount; i++) - { - selectedMissionIndices.Add(msg.ReadByte()); - } - - ushort ownedSubCount = msg.ReadUInt16(); - List ownedSubIndices = new List(); - for (int i = 0; i < ownedSubCount; i++) - { - ownedSubIndices.Add(msg.ReadUInt16()); - } - - bool allowDebugTeleport = msg.ReadBoolean(); - float? reputation = null; - if (msg.ReadBoolean()) { reputation = msg.ReadSingle(); } - - Dictionary factionReps = new Dictionary(); - byte factionsCount = msg.ReadByte(); - for (int i = 0; i < factionsCount; i++) - { - factionReps.Add(msg.ReadIdentifier(), msg.ReadSingle()); - } - - bool forceMapUI = msg.ReadBoolean(); - - bool purchasedHullRepairs = msg.ReadBoolean(); - bool purchasedItemRepairs = msg.ReadBoolean(); - bool purchasedLostShuttles = msg.ReadBoolean(); - - byte missionCount = msg.ReadByte(); - var availableMissions = new List<(Identifier Identifier, byte ConnectionIndex)>(); - for (int i = 0; i < missionCount; i++) - { - Identifier missionIdentifier = msg.ReadIdentifier(); - byte connectionIndex = msg.ReadByte(); - availableMissions.Add((missionIdentifier, connectionIndex)); - } - - var storeBalances = new Dictionary(); - if (msg.ReadBoolean()) - { - byte storeCount = msg.ReadByte(); - for (int i = 0; i < storeCount; i++) - { - Identifier identifier = msg.ReadIdentifier(); - UInt16 storeBalance = msg.ReadUInt16(); - storeBalances.Add(identifier, storeBalance); - } - } - - var buyCrateItems = ReadPurchasedItems(msg, sender: null); - var subSellCrateItems = ReadPurchasedItems(msg, sender: null); - var purchasedItems = ReadPurchasedItems(msg, sender: null); - var soldItems = ReadSoldItems(msg); - - ushort pendingUpgradeCount = msg.ReadUInt16(); - List pendingUpgrades = new List(); - for (int i = 0; i < pendingUpgradeCount; i++) - { - Identifier upgradeIdentifier = msg.ReadIdentifier(); - UpgradePrefab prefab = UpgradePrefab.Find(upgradeIdentifier); - Identifier categoryIdentifier = msg.ReadIdentifier(); - UpgradeCategory category = UpgradeCategory.Find(categoryIdentifier); - int upgradeLevel = msg.ReadByte(); - if (prefab == null || category == null) { continue; } - pendingUpgrades.Add(new PurchasedUpgrade(prefab, category, upgradeLevel)); - } - - ushort purchasedItemSwapCount = msg.ReadUInt16(); - List purchasedItemSwaps = new List(); - for (int i = 0; i < purchasedItemSwapCount; i++) - { - 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; } - purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall)); - } - - bool hasCharacterData = msg.ReadBoolean(); - CharacterInfo myCharacterInfo = null; - if (hasCharacterData) - { - myCharacterInfo = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg); - } + bool refreshCampaignUI = false; if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaignID != campaign.CampaignID) { string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer); - GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign, CampaignSettings.Unsure, mapSeed); + GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign, CampaignSettings.Empty, mapSeed); campaign = (MultiPlayerCampaign)GameMain.GameSession.GameMode; campaign.CampaignID = campaignID; GameMain.NetLobbyScreen.ToggleCampaignMode(true); } //server has a newer save file - if (NetIdUtils.IdMoreRecent(saveID, campaign.PendingSaveID)) - { - campaign.PendingSaveID = saveID; - } - - if (NetIdUtils.IdMoreRecent(updateID, campaign.lastUpdateID)) - { - campaign.SuppressStateSending = true; - campaign.IsFirstRound = isFirstRound; + if (NetIdUtils.IdMoreRecent(saveID, campaign.PendingSaveID)) { campaign.PendingSaveID = saveID; } + campaign.IsFirstRound = isFirstRound; - //we need to have the latest save file to display location/mission/store - if (campaign.LastSaveID == saveID) + if (requiredFlags.HasFlag(NetFlags.Misc)) + { + DebugConsole.Log("Received campaign update (Misc)"); + UInt16 id = msg.ReadUInt16(); + bool purchasedHullRepairs = msg.ReadBoolean(); + bool purchasedItemRepairs = msg.ReadBoolean(); + bool purchasedLostShuttles = msg.ReadBoolean(); + if (ShouldApply(NetFlags.Misc, id, requireUpToDateSave: false)) + { + refreshCampaignUI = campaign.PurchasedHullRepairs != purchasedHullRepairs || + campaign.PurchasedItemRepairs != purchasedItemRepairs || + campaign.PurchasedLostShuttles != purchasedLostShuttles; + campaign.PurchasedHullRepairs = purchasedHullRepairs; + campaign.PurchasedItemRepairs = purchasedItemRepairs; + campaign.PurchasedLostShuttles = purchasedLostShuttles; + } + } + + if (requiredFlags.HasFlag(NetFlags.MapAndMissions)) + { + DebugConsole.Log("Received campaign update (MapAndMissions)"); + UInt16 id = msg.ReadUInt16(); + bool forceMapUI = msg.ReadBoolean(); + bool allowDebugTeleport = msg.ReadBoolean(); + UInt16 currentLocIndex = msg.ReadUInt16(); + UInt16 selectedLocIndex = msg.ReadUInt16(); + + byte missionCount = msg.ReadByte(); + var availableMissions = new List<(Identifier Identifier, byte ConnectionIndex)>(); + for (int i = 0; i < missionCount; i++) + { + Identifier missionIdentifier = msg.ReadIdentifier(); + byte connectionIndex = msg.ReadByte(); + availableMissions.Add((missionIdentifier, connectionIndex)); + } + + byte selectedMissionCount = msg.ReadByte(); + List selectedMissionIndices = new List(); + for (int i = 0; i < selectedMissionCount; i++) + { + selectedMissionIndices.Add(msg.ReadByte()); + } + + if (ShouldApply(NetFlags.MapAndMissions, id, requireUpToDateSave: true)) { campaign.ForceMapUI = forceMapUI; - - UpgradeStore.WaitForServerUpdate = false; - + campaign.Map.AllowDebugTeleport = allowDebugTeleport; campaign.Map.SetLocation(currentLocIndex == UInt16.MaxValue ? -1 : currentLocIndex); campaign.Map.SelectLocation(selectedLocIndex == UInt16.MaxValue ? -1 : selectedLocIndex); - campaign.Map.SelectMission(selectedMissionIndices); - - GameMain.GameSession.OwnedSubmarines.Clear(); - foreach (int ownedSubIndex in ownedSubIndices) - { - SubmarineInfo sub = GameMain.Client.ServerSubmarines[ownedSubIndex]; - if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Owned)) - { - GameMain.GameSession.OwnedSubmarines.Add(sub); - } - } - - campaign.Map.AllowDebugTeleport = allowDebugTeleport; - campaign.CargoManager.SetItemsInBuyCrate(buyCrateItems); - campaign.CargoManager.SetItemsInSubSellCrate(subSellCrateItems); - campaign.CargoManager.SetPurchasedItems(purchasedItems); - campaign.CargoManager.SetSoldItems(soldItems); - foreach (var balance in storeBalances) - { - if (campaign.Map.CurrentLocation.GetStore(balance.Key) is { } store) - { - store.Balance = balance.Value; - } - } - campaign.UpgradeManager.SetPendingUpgrades(pendingUpgrades); - campaign.UpgradeManager.PurchasedUpgrades.Clear(); - foreach (var purchasedItemSwap in purchasedItemSwaps) - { - if (purchasedItemSwap.ItemToInstall == null) - { - campaign.UpgradeManager.CancelItemSwap(purchasedItemSwap.ItemToRemove, force: true); - } - else - { - campaign.UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall, force: true); - } - } - foreach (Item item in Item.ItemList) - { - if (item.PendingItemSwap != null && !purchasedItemSwaps.Any(it => it.ItemToRemove == item)) - { - item.PendingItemSwap = null; - } - } - - foreach (var (identifier, rep) in factionReps) - { - Faction faction = campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier == identifier); - if (faction?.Reputation != null) - { - faction.Reputation.SetReputation(rep); - } - else - { - DebugConsole.ThrowError($"Received an update for a faction that doesn't exist \"{identifier}\"."); - } - } - - if (reputation.HasValue) - { - campaign.Map.CurrentLocation.Reputation.SetReputation(reputation.Value); - campaign?.CampaignUI?.UpgradeStore?.RequestRefresh(); - } - foreach (var availableMission in availableMissions) { MissionPrefab missionPrefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == availableMission.Identifier); @@ -800,36 +682,268 @@ namespace Barotrauma campaign.Map.CurrentLocation.UnlockMission(missionPrefab, connection); } } - - GameMain.NetLobbyScreen.ToggleCampaignMode(true); - } - - bool shouldRefresh = campaign.PurchasedHullRepairs != purchasedHullRepairs || - campaign.PurchasedItemRepairs != purchasedItemRepairs || - campaign.PurchasedLostShuttles != purchasedLostShuttles; - - campaign.PurchasedHullRepairs = purchasedHullRepairs; - campaign.PurchasedItemRepairs = purchasedItemRepairs; - campaign.PurchasedLostShuttles = purchasedLostShuttles; - - if (shouldRefresh) - { - campaign?.CampaignUI?.UpgradeStore?.RequestRefresh(); - } - - if (myCharacterInfo != null) - { - GameMain.Client.CharacterInfo = myCharacterInfo; - GameMain.NetLobbyScreen.SetCampaignCharacterInfo(myCharacterInfo); + campaign.Map.SelectMission(selectedMissionIndices); + ReadStores(msg, apply: true); } else { - GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null); + ReadStores(msg, apply: false); + } + } + + if (requiredFlags.HasFlag(NetFlags.SubList)) + { + DebugConsole.Log("Received campaign update (SubList)"); + UInt16 id = msg.ReadUInt16(); + ushort ownedSubCount = msg.ReadUInt16(); + List ownedSubIndices = new List(); + for (int i = 0; i < ownedSubCount; i++) + { + ownedSubIndices.Add(msg.ReadUInt16()); } - campaign.lastUpdateID = updateID; - campaign.SuppressStateSending = false; + if (ShouldApply(NetFlags.SubList, id, requireUpToDateSave: false)) + { + GameMain.GameSession.OwnedSubmarines.Clear(); + foreach (int ownedSubIndex in ownedSubIndices) + { + SubmarineInfo sub = GameMain.Client.ServerSubmarines[ownedSubIndex]; + if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Owned)) + { + GameMain.GameSession.OwnedSubmarines.Add(sub); + } + } + } } + + if (requiredFlags.HasFlag(NetFlags.UpgradeManager)) + { + DebugConsole.Log("Received campaign update (UpgradeManager)"); + UInt16 id = msg.ReadUInt16(); + + ushort pendingUpgradeCount = msg.ReadUInt16(); + List pendingUpgrades = new List(); + for (int i = 0; i < pendingUpgradeCount; i++) + { + Identifier upgradeIdentifier = msg.ReadIdentifier(); + UpgradePrefab prefab = UpgradePrefab.Find(upgradeIdentifier); + Identifier categoryIdentifier = msg.ReadIdentifier(); + UpgradeCategory category = UpgradeCategory.Find(categoryIdentifier); + int upgradeLevel = msg.ReadByte(); + if (prefab == null || category == null) { continue; } + pendingUpgrades.Add(new PurchasedUpgrade(prefab, category, upgradeLevel)); + } + + ushort purchasedItemSwapCount = msg.ReadUInt16(); + List purchasedItemSwaps = new List(); + for (int i = 0; i < purchasedItemSwapCount; i++) + { + 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; } + purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall)); + } + + if (ShouldApply(NetFlags.UpgradeManager, id, requireUpToDateSave: true)) + { + UpgradeStore.WaitForServerUpdate = false; + campaign.UpgradeManager.SetPendingUpgrades(pendingUpgrades); + campaign.UpgradeManager.PurchasedUpgrades.Clear(); + foreach (var purchasedItemSwap in purchasedItemSwaps) + { + if (purchasedItemSwap.ItemToInstall == null) + { + campaign.UpgradeManager.CancelItemSwap(purchasedItemSwap.ItemToRemove, force: true); + } + else + { + campaign.UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall, force: true); + } + } + foreach (Item item in Item.ItemList) + { + if (item.PendingItemSwap != null && !purchasedItemSwaps.Any(it => it.ItemToRemove == item)) + { + item.PendingItemSwap = null; + } + } + } + } + + + if (requiredFlags.HasFlag(NetFlags.ItemsInBuyCrate)) + { + DebugConsole.Log("Received campaign update (ItemsInBuyCrate)"); + UInt16 id = msg.ReadUInt16(); + var buyCrateItems = ReadPurchasedItems(msg, sender: null); + if (ShouldApply(NetFlags.ItemsInBuyCrate, id, requireUpToDateSave: true)) + { + campaign.CargoManager.SetItemsInBuyCrate(buyCrateItems); + campaign.SetLastUpdateIdForFlag(NetFlags.ItemsInBuyCrate, id); + ReadStores(msg, apply: true); + } + else + { + ReadStores(msg, apply: false); + } + } + if (requiredFlags.HasFlag(NetFlags.ItemsInSellFromSubCrate)) + { + DebugConsole.Log("Received campaign update (ItemsInSellFromSubCrate)"); + UInt16 id = msg.ReadUInt16(); + var subSellCrateItems = ReadPurchasedItems(msg, sender: null); + if (ShouldApply(NetFlags.ItemsInSellFromSubCrate, id, requireUpToDateSave: true)) + { + campaign.CargoManager.SetItemsInSubSellCrate(subSellCrateItems); + campaign.SetLastUpdateIdForFlag(NetFlags.ItemsInSellFromSubCrate, id); + ReadStores(msg, apply: true); + } + else + { + ReadStores(msg, apply: false); + } + } + if (requiredFlags.HasFlag(NetFlags.PurchasedItems)) + { + DebugConsole.Log("Received campaign update (PuchasedItems)"); + UInt16 id = msg.ReadUInt16(); + var purchasedItems = ReadPurchasedItems(msg, sender: null); + if (ShouldApply(NetFlags.PurchasedItems, id, requireUpToDateSave: true)) + { + campaign.CargoManager.SetPurchasedItems(purchasedItems); + campaign.SetLastUpdateIdForFlag(NetFlags.PurchasedItems, id); + ReadStores(msg, apply: true); + } + else + { + ReadStores(msg, apply: false); + } + } + if (requiredFlags.HasFlag(NetFlags.SoldItems)) + { + DebugConsole.Log("Received campaign update (SoldItems)"); + UInt16 id = msg.ReadUInt16(); + var soldItems = ReadSoldItems(msg); + if (ShouldApply(NetFlags.SoldItems, id, requireUpToDateSave: true)) + { + campaign.CargoManager.SetSoldItems(soldItems); + campaign.SetLastUpdateIdForFlag(NetFlags.SoldItems, id); + ReadStores(msg, apply: true); + } + else + { + ReadStores(msg, apply: false); + } + } + if (requiredFlags.HasFlag(NetFlags.Reputation)) + { + DebugConsole.Log("Received campaign update (Reputation)"); + UInt16 id = msg.ReadUInt16(); + float? reputation = null; + if (msg.ReadBoolean()) { reputation = msg.ReadSingle(); } + Dictionary factionReps = new Dictionary(); + byte factionsCount = msg.ReadByte(); + for (int i = 0; i < factionsCount; i++) + { + factionReps.Add(msg.ReadIdentifier(), msg.ReadSingle()); + } + if (ShouldApply(NetFlags.Reputation, id, requireUpToDateSave: true)) + { + if (reputation.HasValue) + { + campaign.Map.CurrentLocation.Reputation.SetReputation(reputation.Value); + campaign?.CampaignUI?.UpgradeStore?.RequestRefresh(); + } + foreach (var (identifier, rep) in factionReps) + { + Faction faction = campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier == identifier); + if (faction?.Reputation != null) + { + faction.Reputation.SetReputation(rep); + } + else + { + DebugConsole.ThrowError($"Received an update for a faction that doesn't exist \"{identifier}\"."); + } + } + } + } + if (requiredFlags.HasFlag(NetFlags.CharacterInfo)) + { + DebugConsole.Log("Received campaign update (CharacterInfo)"); + UInt16 id = msg.ReadUInt16(); + bool hasCharacterData = msg.ReadBoolean(); + CharacterInfo myCharacterInfo = null; + if (hasCharacterData) + { + myCharacterInfo = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg); + } + if (ShouldApply(NetFlags.CharacterInfo, id, requireUpToDateSave: true)) + { + if (myCharacterInfo != null) + { + GameMain.Client.CharacterInfo = myCharacterInfo; + GameMain.NetLobbyScreen.SetCampaignCharacterInfo(myCharacterInfo); + } + else + { + GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null); + } + } + } + + campaign.SuppressStateSending = true; + //we need to have the latest save file to display location/mission/store + if (campaign.LastSaveID == saveID) + { + GameMain.NetLobbyScreen.ToggleCampaignMode(true); + } + if (refreshCampaignUI) + { + campaign?.CampaignUI?.UpgradeStore?.RequestRefresh(); + } + campaign.SuppressStateSending = false; + + bool ShouldApply(NetFlags flag, UInt16 id, bool requireUpToDateSave) + { + if (NetIdUtils.IdMoreRecent(id, campaign.GetLastUpdateIdForFlag(flag)) && + (!requireUpToDateSave || saveID == campaign.LastSaveID)) + { + campaign.SetLastUpdateIdForFlag(flag, id); + return true; + } + else + { + return false; + } + } + + void ReadStores(IReadMessage msg, bool apply) + { + var storeBalances = new Dictionary(); + if (msg.ReadBoolean()) + { + byte storeCount = msg.ReadByte(); + for (int i = 0; i < storeCount; i++) + { + Identifier identifier = msg.ReadIdentifier(); + UInt16 storeBalance = msg.ReadUInt16(); + storeBalances.Add(identifier, storeBalance); + } + } + if (apply) + { + foreach (var balance in storeBalances) + { + if (campaign.Map?.CurrentLocation?.GetStore(balance.Key) is { } store) + { + store.Balance = balance.Value; + } + } + } + } + } public void ClientReadCrew(IReadMessage msg) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 382c43a58..ed5e25c86 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -58,12 +58,12 @@ namespace Barotrauma /// /// Instantiates a new single player campaign /// - private SinglePlayerCampaign(string mapSeed, CampaignSettings settings) : base(GameModePreset.SinglePlayerCampaign) + private SinglePlayerCampaign(string mapSeed, CampaignSettings settings) : base(GameModePreset.SinglePlayerCampaign, settings) { CampaignMetadata = new CampaignMetadata(this); UpgradeManager = new UpgradeManager(this); - map = new Map(this, mapSeed, settings); Settings = settings; + map = new Map(this, mapSeed); foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) { for (int i = 0; i < jobPrefab.InitialCount; i++) @@ -79,7 +79,7 @@ namespace Barotrauma /// /// Loads a previously saved single player campaign from XML /// - private SinglePlayerCampaign(XElement element) : base(GameModePreset.SinglePlayerCampaign) + private SinglePlayerCampaign(XElement element) : base(GameModePreset.SinglePlayerCampaign, CampaignSettings.Empty) { IsFirstRound = false; @@ -87,7 +87,7 @@ namespace Barotrauma { switch (subElement.Name.ToString().ToLowerInvariant()) { - case "campaignsettings": + case CampaignSettings.LowerCaseSaveElementName: Settings = new CampaignSettings(subElement); break; case "crew": @@ -95,7 +95,7 @@ namespace Barotrauma ActiveOrdersElement = subElement.GetChildElement("activeorders"); break; case "map": - map = Map.Load(this, subElement, Settings); + map = Map.Load(this, subElement); break; case "metadata": CampaignMetadata = new CampaignMetadata(this, subElement); @@ -163,21 +163,14 @@ namespace Barotrauma /// /// Start a completely new single player campaign /// - public static SinglePlayerCampaign StartNew(string mapSeed, SubmarineInfo selectedSub, CampaignSettings settings) - { - var campaign = new SinglePlayerCampaign(mapSeed, settings); - return campaign; - } + public static SinglePlayerCampaign StartNew(string mapSeed, CampaignSettings startingSettings) => new SinglePlayerCampaign(mapSeed, startingSettings); /// /// Load a previously saved single player campaign from xml /// /// /// - public static SinglePlayerCampaign Load(XElement element) - { - return new SinglePlayerCampaign(element); - } + public static SinglePlayerCampaign Load(XElement element) => new SinglePlayerCampaign(element); private void InitUI() { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 2245ad99d..490516616 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -64,7 +64,6 @@ namespace Barotrauma public Vector2[] SlotPositions; public static Point SlotSize; public static int Spacing; - public static int HideButtonWidth; private Layout layout; public Layout CurrentLayout @@ -77,64 +76,11 @@ namespace Barotrauma SetSlotPositions(layout); } } - public bool Hidden { get; set; } - - private bool hidePersonalSlots; - private float hidePersonalSlotsState; - private GUIButton hideButton; + private Rectangle personalSlotArea; - public bool HidePersonalSlots - { - get { return hidePersonalSlots; } - } - - public Rectangle PersonalSlotArea - { - get { return personalSlotArea; } - } - - private readonly GUIImage[] indicators = new GUIImage[5]; - private readonly int[] indicatorIndices = new int[5]; - private Vector2 indicatorSpriteSize; - private GUILayoutGroup indicatorGroup; - partial void InitProjSpecific(XElement element) { - Hidden = true; - - hideButton = new GUIButton(new RectTransform(new Point((int)(31f * (HUDLayoutSettings.BottomRightInfoArea.Height / 100f)), HUDLayoutSettings.BottomRightInfoArea.Height), GUI.Canvas) - { AbsoluteOffset = HUDLayoutSettings.CrewArea.Location }, - "", style: "EquipmentToggleButton"); - - indicatorGroup = new GUILayoutGroup(new RectTransform(Point.Zero, hideButton.RectTransform)) { IsHorizontal = false }; - indicatorGroup.ChildAnchor = Anchor.TopCenter; - indicatorSpriteSize = GUIStyle.GetComponentStyle("EquipmentIndicatorDivingSuit").GetDefaultSprite().size; - - indicators[0] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorDivingSuit"); - indicators[1] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorID"); - indicators[2] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorOutfit"); - indicators[3] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorHeadwear"); - indicators[4] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorHeadphones"); - - indicatorIndices[0] = FindLimbSlot(InvSlotType.OuterClothes); - indicatorIndices[1] = FindLimbSlot(InvSlotType.Card); - indicatorIndices[2] = FindLimbSlot(InvSlotType.InnerClothes); - indicatorIndices[3] = FindLimbSlot(InvSlotType.Head); - indicatorIndices[4] = FindLimbSlot(InvSlotType.Headset); - - for (int i = 0; i < indicators.Length; i++) - { - indicators[i].CanBeFocused = false; - } - - hideButton.OnClicked += (GUIButton btn, object userdata) => - { - hidePersonalSlots = !hidePersonalSlots; - return true; - }; - hidePersonalSlots = false; - SlotPositions = new Vector2[SlotTypes.Length]; CurrentLayout = Layout.Default; SetSlotPositions(layout); @@ -271,25 +217,6 @@ namespace Barotrauma return false; } - private void SetIndicatorSizes() - { - indicatorGroup.RectTransform.AbsoluteOffset = new Point((int)Math.Round(4 * GUI.Scale), (int)Math.Round(7 * GUI.Scale)); - indicatorGroup.RectTransform.NonScaledSize = new Point(hideButton.Rect.Width - indicatorGroup.RectTransform.AbsoluteOffset.X * 2, hideButton.Rect.Height - indicatorGroup.RectTransform.AbsoluteOffset.Y * 2); - indicatorGroup.AbsoluteSpacing = (int)Math.Ceiling(2 * GUI.Scale); - - int indicatorHeight = (indicatorGroup.RectTransform.NonScaledSize.Y - indicatorGroup.AbsoluteSpacing * (indicators.Length - 1)) / indicators.Length; - int indicatorWidth = (int)(indicatorSpriteSize.X / (indicatorSpriteSize.Y / indicatorHeight)); - - if (HideButtonWidth % 2 != indicatorWidth % 2) indicatorWidth++; - - Point indicatorSize = new Point(indicatorWidth, indicatorHeight); - - for (int i = 0; i < indicators.Length; i++) - { - indicators[i].RectTransform.NonScaledSize = indicatorSize; - } - } - private void SetSlotPositions(Layout layout) { bool isFourByThree = GUI.IsFourByThree(); @@ -302,13 +229,9 @@ namespace Barotrauma Spacing = (int)(8 * UIScale); } - HideButtonWidth = (int)(31f * (HUDLayoutSettings.BottomRightInfoArea.Height / 100f)); - SlotSize = !isFourByThree ? (SlotSpriteSmall.size * UIScale).ToPoint() : (SlotSpriteSmall.size * UIScale * .925f).ToPoint(); int bottomOffset = SlotSize.Y + Spacing * 2 + ContainedIndicatorHeight; - hideButton.Visible = false; - if (visualSlots == null) { CreateSlots(); } if (visualSlots.None()) { return; } @@ -320,7 +243,7 @@ namespace Barotrauma int normalSlotCount = SlotTypes.Count(s => !PersonalSlots.HasFlag(s) && s != InvSlotType.HealthInterface); int x = GameMain.GraphicsWidth / 2 - normalSlotCount * (SlotSize.X + Spacing) / 2; - int upperX = HUDLayoutSettings.BottomRightInfoArea.X - SlotSize.X - Spacing * 4 - HideButtonWidth; + int upperX = HUDLayoutSettings.BottomRightInfoArea.X - SlotSize.X - Spacing; //make sure the rightmost normal slot doesn't overlap with the personal slots x -= Math.Max((x + normalSlotCount * (SlotSize.X + Spacing)) - (upperX - personalSlotCount * (SlotSize.X + Spacing)), 0); @@ -343,16 +266,6 @@ namespace Barotrauma x += SlotSize.X + Spacing; } } - - if (hideButtonSlotIndex > -1) - { - hideButton.RectTransform.SetPosition(Anchor.TopLeft, Pivot.TopLeft); - hideButton.RectTransform.NonScaledSize = new Point(HideButtonWidth, HUDLayoutSettings.BottomRightInfoArea.Height); - hideButton.RectTransform.AbsoluteOffset = new Point(HUDLayoutSettings.BottomRightInfoArea.Left - HideButtonWidth + GUI.IntScaleCeiling(2f), HUDLayoutSettings.BottomRightInfoArea.Y + GUI.IntScaleCeiling(1f)); - hideButton.Visible = Screen.Selected != GameMain.SubEditorScreen || !GameMain.SubEditorScreen.WiringMode; - - SetIndicatorSizes(); - } } break; case Layout.Right: @@ -533,58 +446,13 @@ namespace Barotrauma bool hoverOnInventory = GUI.MouseOn == null && ((selectedSlot != null && selectedSlot.IsSubSlot) || (DraggingItems.Any() && (DraggingSlot == null || !DraggingSlot.MouseOn()))); - if (CharacterHealth.OpenHealthWindow != null) hoverOnInventory = true; - - if (layout == Layout.Default && (Screen.Selected != GameMain.SubEditorScreen || Screen.Selected is SubEditorScreen editor && editor.WiringMode)) - { - if (hideButton.Visible) - { - hideButton.AddToGUIUpdateList(); - hideButton.UpdateManually(deltaTime, alsoChildren: true); - - hidePersonalSlotsState = hidePersonalSlots ? - Math.Min(hidePersonalSlotsState + deltaTime * 5.0f, 1.0f) : - Math.Max(hidePersonalSlotsState - deltaTime * 5.0f, 0.0f); - - bool personalSlotsMoving = hidePersonalSlotsState > 0 && hidePersonalSlotsState < 1f; - for (int i = 0; i < visualSlots.Length; i++) - { - if (!PersonalSlots.HasFlag(SlotTypes[i])) { continue; } - if (HidePersonalSlots) - { - if (selectedSlot?.Slot == visualSlots[i]) { selectedSlot = null; } - highlightedSubInventorySlots.RemoveWhere(s => s.Slot == visualSlots[i]); - } - visualSlots[i].IsMoving = personalSlotsMoving; - visualSlots[i].DrawOffset = Vector2.Lerp(Vector2.Zero, new Vector2(personalSlotArea.Width, 0.0f), hidePersonalSlotsState); - } - } - } + if (CharacterHealth.OpenHealthWindow != null) { hoverOnInventory = true; } if (hoverOnInventory) { HideTimer = 0.5f; } if (HideTimer > 0.0f) { HideTimer -= deltaTime; } UpdateSlotInput(); - //force personal slots open if an item is running out of battery/fuel/oxygen/etc - if (hidePersonalSlots) - { - for (int i = 0; i < visualSlots.Length; i++) - { - var item = slots[i].FirstOrDefault(); - if (item?.OwnInventory != null && item.OwnInventory.Capacity == 1 && PersonalSlots.HasFlag(SlotTypes[i])) - { - var containedItem = item.OwnInventory.AllItems.FirstOrDefault(); - if (containedItem != null && - containedItem.Condition > 0.0f && - containedItem.Condition / containedItem.MaxCondition < 0.15f) - { - hidePersonalSlots = false; - } - } - } - } - hideSubInventories.Clear(); //remove highlighted subinventory slots that can no longer be accessed highlightedSubInventorySlots.RemoveWhere(s => @@ -653,8 +521,6 @@ namespace Barotrauma if (character == Character.Controlled && character.SelectedCharacter == null) // Permanently open subinventories only available when the default UI layout is in use -> not when grabbing characters { - UpdateEquipmentIndicators(); - //remove the highlighted slots of other characters' inventories when not grabbing anyone highlightedSubInventorySlots.RemoveWhere(s => s.ParentInventory != this && s.ParentInventory?.Owner is Character); @@ -799,40 +665,6 @@ namespace Barotrauma } } } - - private void UpdateEquipmentIndicators() - { - for (int i = 0; i < indicators.Length; i++) - { - if (indicatorIndices[i] < 0) { continue; } - Item item = slots[indicatorIndices[i]].FirstOrDefault(); - if (item != null) - { - Wearable wearable = item.GetComponent(); - if (wearable != null && wearable.DisplayContainedStatus) - { - float conditionPercentage = item.GetContainedItemConditionPercentage(); - - if (conditionPercentage != -1) - { - indicators[i].Color = ToolBox.GradientLerp(conditionPercentage, GUIStyle.EquipmentIndicatorRunningOut, GUIStyle.EquipmentIndicatorEquipped); - } - else - { - indicators[i].Color = GUIStyle.EquipmentIndicatorRunningOut; - } - } - else - { - indicators[i].Color = GUIStyle.EquipmentIndicatorEquipped; - } - } - else - { - indicators[i].Color = GUIStyle.EquipmentIndicatorNotEquipped; - } - } - } private void ShowSubInventory(SlotReference slotRef, float deltaTime, Camera cam, List hideSubInventories, bool isEquippedSubInventory) { @@ -942,6 +774,7 @@ namespace Barotrauma } else { + bool isEquippable = item.AllowedSlots.Any(s => s != InvSlotType.Any); var selectedContainer = character.SelectedConstruction?.GetComponent(); if (selectedContainer != null && selectedContainer.Inventory != null && @@ -967,8 +800,9 @@ namespace Barotrauma return QuickUseAction.TakeFromCharacter; } else if (character.HeldItems.Any(i => - i.OwnInventory != null && - ((i.OwnInventory.CanBePut(item) && allowInventorySwap) || (i.OwnInventory.Capacity == 1 && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item))))) + i.OwnInventory != null && + /*disallow putting into equipped item if the item is equippable (equip as the quick action instead)*/ + ((i.OwnInventory.CanBePut(item) && (allowInventorySwap || !isEquippable)) || (i.OwnInventory.Capacity == 1 && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item))))) { return QuickUseAction.PutToEquippedItem; } @@ -1131,11 +965,18 @@ namespace Barotrauma } break; case QuickUseAction.PutToEquippedItem: + foreach (Item heldItem in character.HeldItems) { if (heldItem.OwnInventory == null) { 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 = + heldItem.OwnInventory.Capacity == 1 && + heldItem.OwnInventory.GetItemAt(0)?.Prefab == item.Prefab && + heldItem.OwnInventory.GetItemsAt(0).Count() > 1; if (heldItem.OwnInventory.TryPutItem(item, Character.Controlled) || - (heldItem.OwnInventory.Capacity == 1 && heldItem.OwnInventory.TryPutItem(item, 0, allowSwapping: true, allowCombine: false, user: Character.Controlled))) + (heldItem.OwnInventory.Capacity == 1 && heldItem.OwnInventory.TryPutItem(item, 0, allowSwapping: !disallowSwapping, allowCombine: false, user: Character.Controlled))) { success = true; for (int j = 0; j < capacity; j++) @@ -1197,11 +1038,6 @@ namespace Barotrauma DrawSlot(spriteBatch, this, visualSlots[i], slots[i].FirstOrDefault(), i, drawItem, SlotTypes[i]); } - - if (hideButton != null && hideButton.Visible && !Locked) - { - hideButton.DrawManually(spriteBatch, alsoChildren: true); - } VisualSlot highlightedQuickUseSlot = null; Rectangle inventoryArea = Rectangle.Empty; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index a189f5470..4966e5c49 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -203,7 +203,7 @@ namespace Barotrauma.Items.Components private float lastMuffleCheckTime; private ItemSound loopingSound; private SoundChannel loopingSoundChannel; - private List playingOneshotSoundChannels = new List(); + private readonly List playingOneshotSoundChannels = new List(); public ItemComponent ReplacedBy; public ItemComponent GetReplacementOrThis() @@ -211,13 +211,16 @@ namespace Barotrauma.Items.Components return ReplacedBy?.GetReplacementOrThis() ?? this; } + public bool NeedsSoundUpdate() + { + if (hasSoundsOfType[(int)ActionType.Always]) { return true; } + if (loopingSoundChannel != null && loopingSoundChannel.IsPlaying) { return true; } + if (playingOneshotSoundChannels.Count > 0) { return true; } + return false; + } + public void UpdateSounds() { - if (!isActive || item.Condition <= 0.0f) - { - StopSounds(ActionType.OnActive); - } - if (loopingSound != null && loopingSoundChannel != null && loopingSoundChannel.IsPlaying) { if (Timing.TotalTime > lastMuffleCheckTime + 0.2f) @@ -280,6 +283,7 @@ namespace Barotrauma.Items.Components loopingSound.RoundSound.GetRandomFrequencyMultiplier(), SoundPlayer.ShouldMuffleSound(Character.Controlled, item.WorldPosition, loopingSound.Range, Character.Controlled?.CurrentHull)); loopingSoundChannel.Looping = true; + item.CheckNeedsSoundUpdate(this); //TODO: tweak loopingSoundChannel.Near = loopingSound.Range * 0.4f; loopingSoundChannel.Far = loopingSound.Range; @@ -298,7 +302,6 @@ namespace Barotrauma.Items.Components loopingSound = null; } } - return; } @@ -333,6 +336,7 @@ namespace Barotrauma.Items.Components } PlaySound(matchingSounds[index], item.WorldPosition); + item.CheckNeedsSoundUpdate(this); } } private void PlaySound(ItemSound itemSound, Vector2 position) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 7af73e740..90849cd19 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -108,6 +108,7 @@ namespace Barotrauma.Items.Components itemList = new GUIListBox(new RectTransform(new Vector2(1f, 0.9f), paddedItemFrame.RectTransform), style: null) { + PlaySoundOnSelect = true, OnSelected = (component, userdata) => { selectedItem = userdata as FabricationRecipe; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index d14a9b237..371d3882b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -333,6 +333,7 @@ namespace Barotrauma.Items.Components GUIListBox listBox = new GUIListBox(new RectTransform(Vector2.One, searchAutoComplete.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = (component, o) => { if (o is ItemPrefab prefab) @@ -744,11 +745,11 @@ namespace Barotrauma.Items.Components if (key == Keys.Down) { - listBox.SelectNext(true, autoScroll: true); + listBox.SelectNext(force: GUIListBox.Force.Yes, playSelectSound: GUIListBox.PlaySelectSound.Yes); } else if (key == Keys.Up) { - listBox.SelectPrevious(true, autoScroll: true); + listBox.SelectPrevious(force: GUIListBox.Force.Yes, playSelectSound: GUIListBox.PlaySelectSound.Yes); } else if (key == Keys.Enter) { @@ -782,7 +783,7 @@ namespace Barotrauma.Items.Components if (component.Visible && first) { - listBox.Select(i, force: true, autoScroll: false); + listBox.Select(i, GUIListBox.Force.Yes, GUIListBox.AutoScroll.Disabled); first = false; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs index a3724b8a3..2798830ae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs @@ -18,7 +18,7 @@ namespace Barotrauma.Items.Components } GuiFrame = selectionUI.GuiFrame; - selectionUI.RefreshSubmarineDisplay(true); + selectionUI.RefreshSubmarineDisplay(true, setTransferOptionToTrue: true); IsActive = true; return base.Select(character); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 2fbf7b38d..d85bca980 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -927,6 +927,8 @@ namespace Barotrauma.Items.Components bool autoPilot = msg.ReadBoolean(); bool dockingButtonClicked = msg.ReadBoolean(); + ushort userID = msg.ReadUInt16(); + Vector2 newSteeringInput = steeringInput; Vector2 newTargetVelocity = targetVelocity; float newSteeringAdjustSpeed = steeringAdjustSpeed; @@ -935,7 +937,7 @@ namespace Barotrauma.Items.Components if (dockingButtonClicked) { - item.SendSignal("1", "toggle_docking"); + item.SendSignal(new Signal("1", sender: Entity.FindEntityByID(userID) as Character), "toggle_docking"); } if (autoPilot) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Planter.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Planter.cs index de9dc05fe..c0d347358 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Planter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Planter.cs @@ -40,8 +40,6 @@ namespace Barotrauma.Items.Components } } - private LightComponent lightComponent; - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { for (var i = 0; i < GrowableSeeds.Length; i++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index d1a295c91..156e1afc1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -418,7 +418,7 @@ namespace Barotrauma.Items.Components if (!GameMain.IsMultiplayer) { RepairBoost(qteSuccess); } - SoundPlayer.PlayUISound(qteSuccess ? GUISoundType.IncreaseQuantity : GUISoundType.DecreaseQuantity); + SoundPlayer.PlayUISound(qteSuccess ? GUISoundType.Increase : GUISoundType.Decrease); //on failure during cooldown reset cursor to beginning if (!qteSuccess && qteCooldown > 0.0f) { qteTimer = QteDuration; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index 8c7ef71ea..8b97d27be 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -100,7 +100,7 @@ namespace Barotrauma.Items.Components GUITextBlock newBlock = new GUITextBlock( new RectTransform(new Vector2(1, 0), historyBox.Content.RectTransform, anchor: Anchor.TopCenter), "> " + input, - textColor: color, wrap: true, font: UseMonospaceFont ? GUIStyle.MonospacedFont : GUIStyle.GlobalFont) + textColor: color, wrap: true, font: UseMonospaceFont ? GUIStyle.MonospacedFont : GUIStyle.Font) { CanBeFocused = false }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index bda201aa8..32610f313 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -569,6 +569,18 @@ namespace Barotrauma } } + public void CheckNeedsSoundUpdate(ItemComponent ic) + { + if (ic.NeedsSoundUpdate()) + { + if (!updateableComponents.Contains(ic)) + { + updateableComponents.Add(ic); + } + isActive = true; + } + } + public void UpdateSpriteStates(float deltaTime) { if (activeContainedSprite != null) @@ -940,6 +952,7 @@ namespace Barotrauma var textList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), msgBox.Content.RectTransform, Anchor.TopCenter)) { + PlaySoundOnSelect = true, OnSelected = (component, userData) => { if (!(userData is Identifier)) { return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 651f40201..ecdbeeacc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -98,7 +98,7 @@ namespace Barotrauma OnClicked = (btn, userData) => { Rand.SetSyncedSeed(ToolBox.StringToInt(this.Seed)); - Generate(); + Generate(GameMain.GameSession.GameMode is CampaignMode campaign ? campaign.Settings : CampaignSettings.Empty); InitProjectSpecific(); return true; } @@ -642,11 +642,11 @@ namespace Barotrauma } } - if (GameMain.DebugDraw && location == HighlightedLocation && (!location.Discovered || !location.HasOutpost())) + if (GameMain.DebugDraw) { - if (location.Reputation != null) + Vector2 dPos = pos; + if (location == HighlightedLocation && (!location.Discovered || !location.HasOutpost()) && location.Reputation != null) { - Vector2 dPos = pos; dPos.Y += 48; string name = $"Reputation: {location.Name}"; Vector2 nameSize = GUIStyle.SmallFont.MeasureString(name); @@ -663,6 +663,8 @@ namespace Barotrauma GUI.DrawString(spriteBatch, dPos + (new Vector2(256, 32) / 2) - (repValueSize / 2), reputationValue, Color.White, Color.Black, font: GUIStyle.SubHeadingFont); GUI.DrawRectangle(spriteBatch, new Rectangle((int)dPos.X, (int)dPos.Y, 256, 32), Color.White); } + dPos.Y += 48; + GUI.DrawString(spriteBatch, dPos, $"Difficulty: {location.LevelData.Difficulty.FormatZeroDecimal()}", Color.White, Color.Black * 0.8f, 4, font: GUIStyle.SmallFont); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs index c8e5cedeb..d0a908933 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs @@ -154,13 +154,13 @@ namespace Barotrauma crewSizeText.RectTransform.MinSize = new Point(0, crewSizeText.Children.First().Rect.Height); } - if (!string.IsNullOrEmpty(RecommendedCrewExperience)) + if (RecommendedCrewExperience != CrewExperienceLevel.Unknown) { var crewExperienceText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform), TextManager.Get("RecommendedCrewExperience"), textAlignment: Alignment.TopLeft, font: font, wrap: true) { CanBeFocused = false }; new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), crewExperienceText.RectTransform, Anchor.TopRight, Pivot.TopLeft), - TextManager.Get(RecommendedCrewExperience), textAlignment: Alignment.TopLeft, font: font, wrap: true) + TextManager.Get(RecommendedCrewExperience.ToIdentifier()), textAlignment: Alignment.TopLeft, font: font, wrap: true) { CanBeFocused = false }; crewExperienceText.RectTransform.MinSize = new Point(0, crewExperienceText.Children.First().Rect.Height); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index ad05d0b20..e72fc7fb1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -100,12 +100,16 @@ namespace Barotrauma GUIListBox specsContainer = null; new GUICustomComponent(new RectTransform(Vector2.One, innerPadded.RectTransform, Anchor.Center), - (spriteBatch, component) => { + (spriteBatch, component) => + { + if (isDisposed) { return; } camera.UpdateTransform(interpolate: true, updateListener: false); Rectangle drawRect = new Rectangle(component.Rect.X + 1, component.Rect.Y + 1, component.Rect.Width - 2, component.Rect.Height - 2); RenderSubmarine(spriteBatch, drawRect, component); }, - (deltaTime, component) => { + (deltaTime, component) => + { + if (isDisposed) { return; } bool isMouseOnComponent = GUI.MouseOn == component; camera.MoveCamera(deltaTime, allowZoom: isMouseOnComponent, followSub: false); if (isMouseOnComponent && @@ -294,8 +298,8 @@ namespace Barotrauma private void BakeMapEntity(XElement element) { - string identifier = element.GetAttributeString("identifier", ""); - if (string.IsNullOrEmpty(identifier)) { return; } + Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); + if (identifier.IsEmpty) { return; } Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty); if (rect.Equals(Rectangle.Empty)) { return; } @@ -308,7 +312,16 @@ namespace Barotrauma float rotation = element.GetAttributeFloat("rotation", 0f); - MapEntityPrefab prefab = MapEntityPrefab.List.FirstOrDefault(p => p.Identifier == identifier); + MapEntityPrefab prefab = null; + if (element.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase) && + ItemPrefab.Prefabs.TryGet(identifier, out ItemPrefab ip)) + { + prefab = ip; + } + else + { + prefab = MapEntityPrefab.List.FirstOrDefault(p => p.Identifier == identifier); + } if (prefab == null) { return; } var texture = prefab.Sprite.Texture; @@ -329,7 +342,6 @@ namespace Barotrauma bool overrideSprite = false; ItemPrefab itemPrefab = prefab as ItemPrefab; - StructurePrefab structurePrefab = prefab as StructurePrefab; if (itemPrefab != null) { BakeItemComponents(itemPrefab, rect, color, scale, rotation, depth, out overrideSprite); @@ -337,7 +349,7 @@ namespace Barotrauma if (!overrideSprite) { - if (structurePrefab != null) + if (prefab is StructurePrefab structurePrefab) { ParseUpgrades(structurePrefab.ConfigElement, ref scale); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 2e36c70de..797640696 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -689,7 +689,7 @@ namespace Barotrauma.Networking if (ChildServerRelay.Process?.HasExited ?? true) { Disconnect(); - if (!GUIMessageBox.MessageBoxes.Any(mb => (mb as GUIMessageBox)?.Text.Text == ChildServerRelay.CrashMessage)) + if (!GUIMessageBox.MessageBoxes.Any(mb => (mb as GUIMessageBox)?.Text?.Text == ChildServerRelay.CrashMessage)) { var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu; @@ -824,7 +824,11 @@ namespace Barotrauma.Networking byte campaignID = inc.ReadByte(); UInt16 campaignSaveID = inc.ReadUInt16(); - UInt16 campaignUpdateID = inc.ReadUInt16(); + Dictionary campaignUpdateIDs = new Dictionary(); + foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + campaignUpdateIDs[flag] = inc.ReadUInt16(); + } IWriteMessage readyToStartMsg = new WriteOnlyMessage(); readyToStartMsg.Write((byte)ClientPacketHeader.RESPONSE_STARTGAME); @@ -843,7 +847,7 @@ namespace Barotrauma.Networking campaign != null && campaign.CampaignID == campaignID && campaign.LastSaveID == campaignSaveID && - campaign.LastUpdateID == campaignUpdateID; + campaignUpdateIDs.All(kvp => campaign.GetLastUpdateIdForFlag(kvp.Key) == kvp.Value); } readyToStartMsg.Write(readyToStart); @@ -2401,7 +2405,10 @@ namespace Barotrauma.Networking { outmsg.Write(campaign.LastSaveID); outmsg.Write(campaign.CampaignID); - outmsg.Write(campaign.LastUpdateID); + foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + outmsg.Write(campaign.GetLastUpdateIdForFlag(netFlag)); + } outmsg.Write(GameMain.NetLobbyScreen.CampaignCharacterDiscarded); } @@ -2446,7 +2453,10 @@ namespace Barotrauma.Networking { outmsg.Write(campaign.LastSaveID); outmsg.Write(campaign.CampaignID); - outmsg.Write(campaign.LastUpdateID); + foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + outmsg.Write(campaign.GetLastUpdateIdForFlag(flag)); + } outmsg.Write(GameMain.NetLobbyScreen.CampaignCharacterDiscarded); } @@ -2644,7 +2654,7 @@ namespace Barotrauma.Networking if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.CampaignID != campaignID) { string savePath = transfer.FilePath; - GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign, CampaignSettings.Unsure); + GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign, CampaignSettings.Empty); campaign = (MultiPlayerCampaign)GameMain.GameSession.GameMode; campaign.CampaignID = campaignID; GameMain.NetLobbyScreen.ToggleCampaignMode(true); @@ -2674,9 +2684,12 @@ namespace Barotrauma.Networking } DebugConsole.Log("Campaign save received (" + GameMain.GameSession.SavePath + "), save ID " + campaign.LastSaveID); - //decrement campaign update ID so the server will send us the latest data + //decrement campaign update IDs so the server will send us the latest data //(as there may have been campaign updates after the save file was created) - campaign.LastUpdateID--; + foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + campaign.SetLastUpdateIdForFlag(flag, (ushort)(campaign.GetLastUpdateIdForFlag(flag) - 1)); + } break; case FileTransferType.Mod: if (!(Screen.Selected is ModDownloadScreen)) { return; } @@ -2775,6 +2788,15 @@ namespace Barotrauma.Networking GameMain.GameSession = null; } + public void SendCharacterInfo() + { + IWriteMessage msg = new WriteOnlyMessage(); + msg.Write((byte)ClientPacketHeader.UPDATE_CHARACTERINFO); + WriteCharacterInfo(msg); + msg.Write((byte)ServerNetObject.END_OF_MESSAGE); + clientPeer?.Send(msg, DeliveryMethod.Reliable); + } + public void WriteCharacterInfo(IWriteMessage msg) { msg.Write(characterInfo == null); @@ -2824,18 +2846,18 @@ namespace Barotrauma.Networking } #region Submarine Change Voting - public void InitiateSubmarineChange(SubmarineInfo sub, VoteType voteType) + public void InitiateSubmarineChange(SubmarineInfo sub, bool transferItems, VoteType voteType) { if (sub == null) { return; } - Vote(voteType, sub); + Vote(voteType, (sub, transferItems)); } - public void ShowSubmarineChangeVoteInterface(Client starter, SubmarineInfo info, VoteType type, float timeOut) + public void ShowSubmarineChangeVoteInterface(Client starter, SubmarineInfo info, VoteType type, bool transferItems, float timeOut) { if (info == null) { return; } if (votingInterface != null && votingInterface.VoteRunning) { return; } votingInterface?.Remove(); - votingInterface = VotingInterface.CreateSubmarineVotingInterface(starter, info, type, timeOut); + votingInterface = VotingInterface.CreateSubmarineVotingInterface(starter, info, type, transferItems, timeOut); } #endregion @@ -3014,7 +3036,7 @@ namespace Barotrauma.Networking msg.Write(mapSeed); msg.Write(sub.Name); msg.Write(sub.MD5Hash.StringRepresentation); - settings.Serialize(msg); + msg.Write(settings); clientPeer.Send(msg, DeliveryMethod.Reliable); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs index 09cb93edc..6e097aabf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -111,7 +111,7 @@ namespace Barotrauma.Networking timeout = Screen.Selected == GameMain.GameScreen ? NetworkConnection.TimeoutThresholdInGame : NetworkConnection.TimeoutThreshold; - + PacketHeader packetHeader = (PacketHeader)data[0]; if (!packetHeader.IsServerMessage()) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index 04c501909..2f5c2f480 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -11,7 +11,13 @@ namespace Barotrauma.Networking private bool isActive; private readonly UInt64 selfSteamID; + private UInt64 ownerKey64 => unchecked((UInt64)ownerKey); + private UInt64 ReadSteamId(IReadMessage inc) + => inc.ReadUInt64() ^ ownerKey64; + private void WriteSteamId(IWriteMessage msg, UInt64 val) + => msg.Write(val ^ ownerKey64); + private long sentBytes, receivedBytes; class RemotePeer @@ -58,6 +64,8 @@ namespace Barotrauma.Networking { if (isActive) { return; } + this.ownerKey = ownerKey; + initializationStep = ConnectionInitialization.SteamTicketAndVersion; ServerConnection = new PipeConnection(selfSteamID); @@ -103,7 +111,7 @@ namespace Barotrauma.Networking //known now int prevBitPosition = msg.Message.BitPosition; msg.Message.BitPosition = sizeof(ulong) * 8; - msg.Message.Write(ownerID); + WriteSteamId(msg.Message, ownerID); msg.Message.BitPosition = prevBitPosition; byte[] msgToSend = (byte[])msg.Message.Buffer.Clone(); Array.Resize(ref msgToSend, msg.Message.LengthBytes); @@ -141,8 +149,8 @@ namespace Barotrauma.Networking } IWriteMessage outMsg = new WriteOnlyMessage(); - outMsg.Write(steamId); - outMsg.Write(remotePeer.OwnerSteamID); + WriteSteamId(outMsg, steamId); + WriteSteamId(outMsg, remotePeer.OwnerSteamID); outMsg.Write(data, 1, dataLength - 1); DeliveryMethod deliveryMethod = (DeliveryMethod)data[0]; @@ -232,7 +240,7 @@ namespace Barotrauma.Networking { if (!isActive) { return; } - UInt64 recipientSteamId = inc.ReadUInt64(); + UInt64 recipientSteamId = ReadSteamId(inc); DeliveryMethod deliveryMethod = (DeliveryMethod)inc.ReadByte(); int p2pDataStart = inc.BytePosition; @@ -343,8 +351,8 @@ namespace Barotrauma.Networking if (packetHeader.IsConnectionInitializationStep()) { IWriteMessage outMsg = new WriteOnlyMessage(); - outMsg.Write(selfSteamID); - outMsg.Write(selfSteamID); + WriteSteamId(outMsg, selfSteamID); + WriteSteamId(outMsg, selfSteamID); outMsg.Write((byte)(PacketHeader.IsConnectionInitializationStep)); outMsg.Write(Name); @@ -436,8 +444,8 @@ namespace Barotrauma.Networking IWriteMessage msgToSend = new WriteOnlyMessage(); byte[] msgData = new byte[msg.LengthBytes]; msg.PrepareForSending(ref msgData, compressPastThreshold, out bool isCompressed, out int length); - msgToSend.Write(selfSteamID); - msgToSend.Write(selfSteamID); + WriteSteamId(msgToSend, selfSteamID); + WriteSteamId(msgToSend, selfSteamID); msgToSend.Write((byte)(isCompressed ? PacketHeader.IsCompressed : PacketHeader.None)); msgToSend.Write((UInt16)length); msgToSend.Write(msgData, 0, length); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index de7227cc8..a889a9b4d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -7,6 +7,20 @@ namespace Barotrauma { partial class Voting { + private struct SubmarineVoteInfo + { + public SubmarineInfo SubmarineInfo { get; set; } + public bool TransferItems { get; set; } + public int DeliveryFee { get; set; } + + public SubmarineVoteInfo(SubmarineInfo submarineInfo, bool transferItems, int deliveryFee) + { + SubmarineInfo = submarineInfo; + TransferItems = transferItems; + DeliveryFee = deliveryFee; + } + } + private readonly Dictionary voteCountYes = new Dictionary(), voteCountNo = new Dictionary(), @@ -131,14 +145,16 @@ namespace Barotrauma case VoteType.PurchaseAndSwitchSub: case VoteType.PurchaseSub: case VoteType.SwitchSub: - if (data is SubmarineInfo voteSub) + if (data is (SubmarineInfo voteSub, bool transferItems)) { //initiate sub vote msg.Write(true); msg.Write(voteSub.Name); + msg.Write(transferItems); } else { + // vote if (!(data is int)) { return; } msg.Write(false); msg.Write((int)data); @@ -246,7 +262,7 @@ namespace Barotrauma float timeOut = inc.ReadByte(); Client myClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == GameMain.Client.ID); - if (!myClient.InGame) { return; } + if (myClient == null || !myClient.InGame) { return; } switch (voteType) { @@ -254,13 +270,14 @@ namespace Barotrauma case VoteType.PurchaseAndSwitchSub: case VoteType.SwitchSub: string subName1 = inc.ReadString(); + bool transferItems = inc.ReadBoolean(); SubmarineInfo info = GameMain.Client.ServerSubmarines.FirstOrDefault(s => s.Name == subName1); if (info == null) { DebugConsole.ThrowError("Failed to find a matching submarine, vote aborted"); return; } - GameMain.Client.ShowSubmarineChangeVoteInterface(starterClient, info, voteType, timeOut); + GameMain.Client.ShowSubmarineChangeVoteInterface(starterClient, info, voteType, transferItems, timeOut); break; case VoteType.TransferMoney: byte fromClientId = inc.ReadByte(); @@ -279,39 +296,40 @@ namespace Barotrauma case VoteState.Passed: case VoteState.Failed: bool passed = inc.ReadBoolean(); - - SubmarineInfo subInfo = null; + SubmarineVoteInfo submarineVoteInfo = default; switch (voteType) { case VoteType.PurchaseSub: case VoteType.PurchaseAndSwitchSub: case VoteType.SwitchSub: string subName2 = inc.ReadString(); - subInfo = GameMain.Client.ServerSubmarines.FirstOrDefault(s => s.Name == subName2); - if (subInfo == null) + var submarineInfo = GameMain.Client.ServerSubmarines.FirstOrDefault(s => s.Name == subName2); + bool transferItems = inc.ReadBoolean(); + int deliveryFee = inc.ReadInt16(); + if (submarineInfo == null) { DebugConsole.ThrowError("Failed to find a matching submarine, vote aborted"); return; } + submarineVoteInfo = new SubmarineVoteInfo(submarineInfo, transferItems, deliveryFee); break; } GameMain.Client.VotingInterface?.EndVote(passed, yesClientCount, noClientCount); - if (passed && subInfo != null) + if (passed && submarineVoteInfo.SubmarineInfo is { } subInfo) { - int deliveryFee = inc.ReadInt16(); switch (voteType) { case VoteType.PurchaseAndSwitchSub: GameMain.GameSession.PurchaseSubmarine(subInfo); - GameMain.GameSession.SwitchSubmarine(subInfo, 0); + GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems, 0); break; case VoteType.PurchaseSub: GameMain.GameSession.PurchaseSubmarine(subInfo); break; case VoteType.SwitchSub: - GameMain.GameSession.SwitchSubmarine(subInfo, deliveryFee); + GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems, submarineVoteInfo.DeliveryFee); break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index c7cd94394..38cda4a69 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -1,8 +1,11 @@ +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; +using System.Collections.Immutable; using System.Linq; +using System.Xml.Linq; namespace Barotrauma { @@ -15,11 +18,11 @@ namespace Barotrauma protected GUITextBox saveNameBox, seedBox; protected GUIButton loadGameButton; - + public Action StartNewGame; public Action LoadGame; - protected enum CategoryFilter { All = 0, Vanilla = 1, Custom = 2 }; + protected enum CategoryFilter { All = 0, Vanilla = 1, Custom = 2 } protected CategoryFilter subFilter = CategoryFilter.All; public GUIButton StartButton @@ -33,15 +36,11 @@ namespace Barotrauma get; protected set; } - - public GUITickBox EnableRadiationToggle { get; set; } - public GUILayoutGroup CampaignSettingsContent { get; set; } + public CampaignSettings CurrentSettings = new CampaignSettings(element: null); public GUIButton CampaignCustomizeButton { get; set; } public GUIMessageBox CampaignCustomizeSettings { get; set; } - public GUITextBlock MaxMissionCountText; - public CampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer) { this.newGameContainer = newGameContainer; @@ -102,5 +101,259 @@ namespace Barotrauma return saveFrame; } + + public struct CampaignSettingElements + { + public SettingValue RadiationEnabled; + public SettingValue MaxMissionCount; + public SettingValue StartingFunds; + public SettingValue Difficulty; + public SettingValue StartItemSet; + + public CampaignSettings CreateSettings() + { + return new CampaignSettings(element: null) + { + RadiationEnabled = RadiationEnabled.GetValue(), + MaxMissionCount = MaxMissionCount.GetValue(), + StartingBalanceAmount = StartingFunds.GetValue(), + Difficulty = Difficulty.GetValue(), + StartItemSet = StartItemSet.GetValue() + }; + } + } + + public readonly struct SettingValue + { + private readonly Func getter; + private readonly Action setter; + + public T GetValue() + { + return getter.Invoke(); + } + + public void SetValue(T value) + { + setter.Invoke(value); + } + + public SettingValue(Func get, Action set) + { + getter = get; + setter = set; + } + } + + private readonly struct SettingCarouselElement + { + public readonly LocalizedString Label; + public readonly T Value; + public readonly bool IsHidden; + + public SettingCarouselElement(T value, string label, bool isHidden = false) + { + Value = value; + Label = TextManager.Get(label).Fallback(label); + IsHidden = isHidden; + } + } + + protected static CampaignSettingElements CreateCampaignSettingList(GUIComponent parent, CampaignSettings prevSettings) + { + const float verticalSize = 0.14f; + + 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); + + presetDropdownLayout.RectTransform.MinSize = new Point(0, presetDropdown.Rect.Height); + + foreach (CampaignSettings settings in CampaignModePresets.List) + { + string name = settings.PresetName; + presetDropdown.AddItem(TextManager.Get($"preset.{name}").Fallback(name), settings); + } + + GUIListBox settingsList = new GUIListBox(new RectTransform(new Vector2(1f, 1f - verticalSize), parent.RectTransform)) + { + Spacing = GUI.IntScale(5) + }; + + SettingValue radiationEnabled = CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableRadiation"), TextManager.Get("campaignoption.enableradiation.tooltip"), prevSettings.RadiationEnabled, verticalSize); + + 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); + + ImmutableArray> fundOptions = ImmutableArray.Create( + new SettingCarouselElement(StartingBalanceAmount.High, "startingfunds.high"), + new SettingCarouselElement(StartingBalanceAmount.Medium, "startingfunds.medium"), + new SettingCarouselElement(StartingBalanceAmount.Low, "startingfunds.low") + ); + + 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); + + 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); + + SettingValue maxMissionCountInput = CreateGUINumberInputCarousel(settingsList.Content, TextManager.Get("maxmissioncount"), TextManager.Get("maxmissioncounttooltip"), prevSettings.MaxMissionCount, valueStep: 1, verticalSize); + + presetDropdown.OnSelected = (selected, o) => + { + if (o is CampaignSettings settings) + { + radiationEnabled.SetValue(settings.RadiationEnabled); + maxMissionCountInput.SetValue(settings.MaxMissionCount); + startingFundsInput.SetValue(settings.StartingBalanceAmount); + difficultyInput.SetValue(settings.Difficulty); + startingSetInput.SetValue(settings.StartItemSet); + return true; + } + return false; + }; + + return new CampaignSettingElements + { + RadiationEnabled = radiationEnabled, + MaxMissionCount = maxMissionCountInput, + StartingFunds = startingFundsInput, + Difficulty = difficultyInput, + StartItemSet = startingSetInput + }; + + // 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, float 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) + { + IntValue = defaultValue + }; + 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; + + bool ChangeValue(GUIButton btn, object userData) + { + if (!(userData is int change)) { return false; } + + numberInput.IntValue += change; + return true; + } + + return new SettingValue(() => numberInput.IntValue, i => numberInput.IntValue = i); + } + + static SettingValue CreateSelectionCarousel(GUIComponent parent, LocalizedString description, LocalizedString tooltip, SettingCarouselElement defaultValue, float verticalSize, + ImmutableArray> options) + { + 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) { 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) + { + IntValue = options.IndexOf(defaultValue), + MinValueInt = 0, + MaxValueInt = options.Length, + Visible = false + }; + 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) + { + CanBeFocused = false + }; + + GUIButton plusButton = new GUIButton(new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton", textAlignment: Alignment.Center) { UserData = 1 }; + + minusButton.OnClicked = plusButton.OnClicked = ChangeValue; + + bool ChangeValue(GUIButton btn, object userData) + { + if (!(userData is int change)) { return false; } + + int hiddenOptions = 0; + + for (int i = options.Length - 1; i >= 0; i--) + { + if (options[i].IsHidden) + { + hiddenOptions++; + continue; + } + break; + } + + int limit = options.Length - hiddenOptions; + + if (PlayerInput.IsShiftDown()) + { + limit = options.Length; + } + + int newValue = MathUtils.PositiveModulo(Math.Clamp(numberInput.IntValue + change, min: -1, max: limit), limit); + SetValue(newValue); + return true; + } + + void SetValue(int value) + { + numberInput.IntValue = value; + inputLabel.Text = options[value].Label.Value; + } + + 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) + { + 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) + { + Selected = defaultValue, + ToolTip = tooltip + }; + tickBox.Box.IgnoreLayoutGroups = true; + tickBox.Box.RectTransform.SetPosition(Anchor.CenterRight); + inputContainer.RectTransform.Parent.MinSize = new Point(0, tickBox.RectTransform.MinSize.Y); + return new SettingValue(() => tickBox.Selected, b => tickBox.Selected = b); + } + + 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) + { + RelativeSpacing = 0.05f, + Stretch = true + }; + inputContainer.RectTransform.IsFixedSize = true; + settingHolder.RectTransform.MinSize = new Point(0, (int)descriptionBlock.TextSize.Y); + return inputContainer; + } + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs index 97e08e561..0c297bbf1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -18,71 +18,35 @@ namespace Barotrauma var verticalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, newGameContainer.RectTransform), isHorizontal: false) { Stretch = true, - RelativeSpacing = 0.0f + RelativeSpacing = 0.05f + }; + + GUILayoutGroup nameSeedLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), verticalLayout.RectTransform), isHorizontal: false) + { + Stretch = true + }; + + GUILayoutGroup campaignSettingLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), verticalLayout.RectTransform), isHorizontal: false) + { + Stretch = true, + RelativeSpacing = 0.05f }; // New game - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SaveName"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); - saveNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, string.Empty) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SaveName"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); + saveNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, string.Empty) { - textFilterFunction = (string str) => { return ToolBox.RemoveInvalidFileNameChars(str); } + textFilterFunction = ToolBox.RemoveInvalidFileNameChars }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); - seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, ToolBox.RandomSeed(8)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); + seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, ToolBox.RandomSeed(8)); - GUIFrame radiationBoxContainer - = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), verticalLayout.RectTransform), style: null); - GUITickBox radiationEnabledTickBox = null; - if (MapGenerationParams.Instance.RadiationParams != null) - { - radiationEnabledTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.5f), radiationBoxContainer.RectTransform, Anchor.Center), TextManager.Get("CampaignOption.EnableRadiation"), font: GUIStyle.Font) - { - Selected = true, - OnSelected = box => true - }; - } + nameSeedLayout.RectTransform.MinSize = new Point(0, nameSeedLayout.Children.Sum(c => c.RectTransform.MinSize.Y)); - var maxMissionCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), verticalLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; - var maxMissionCountDescription = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), maxMissionCountSettingHolder.RectTransform), TextManager.Get("maxmissioncount", "missions"), wrap: true) - { - ToolTip = TextManager.Get("maxmissioncounttooltip") - }; - int maxMissionCount = GameMain.NetworkMember.ServerSettings.MaxMissionCount; - var maxMissionCountContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), maxMissionCountSettingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; - var maxMissionCountButtons = new GUIButton[2]; - maxMissionCountButtons[0] - = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), maxMissionCountContainer.RectTransform), - style: "GUIButtonToggleLeft"); - var maxMissionCountText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), maxMissionCountContainer.RectTransform), "0", textAlignment: Alignment.Center, style: "GUITextBox"); + CampaignSettingElements elements = CreateCampaignSettingList(campaignSettingLayout, CampaignSettings.Empty); - void updateMissionCountText() - { - maxMissionCount = MathHelper.Clamp(maxMissionCount, - CampaignSettings.MinMissionCountLimit, - CampaignSettings.MaxMissionCountLimit); - - maxMissionCountText.Text = maxMissionCount.ToString(CultureInfo.InvariantCulture); - } - maxMissionCountButtons[1] - = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), maxMissionCountContainer.RectTransform), - style: "GUIButtonToggleRight"); - maxMissionCountButtons[0].OnClicked = (button, o) => - { - maxMissionCount--; - updateMissionCountText(); - return false; - }; - maxMissionCountButtons[1].OnClicked = (button, o) => - { - maxMissionCount++; - updateMissionCountText(); - return false; - }; - updateMissionCountText(); - maxMissionCountSettingHolder.Children.ForEach(c => c.ToolTip = maxMissionCountSettingHolder.ToolTip); - - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.04f), + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), verticalLayout.RectTransform) { MaxSize = new Point(int.MaxValue, 60) }, childAnchor: Anchor.BottomRight, isHorizontal: true); StartButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1f), buttonContainer.RectTransform, Anchor.BottomRight), TextManager.Get("StartCampaignButton")) @@ -99,7 +63,7 @@ namespace Barotrauma if (GameMain.NetLobbyScreen.SelectedSub == null) { return false; } selectedSub = GameMain.NetLobbyScreen.SelectedSub; - + if (selectedSub.SubmarineClass == SubmarineClass.Undefined) { new GUIMessageBox(TextManager.Get("error"), TextManager.Get("undefinedsubmarineselected")); @@ -115,11 +79,7 @@ namespace Barotrauma string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveNameBox.Text); bool hasRequiredContentPackages = selectedSub.RequiredContentPackagesInstalled; - CampaignSettings settings = new CampaignSettings - { - RadiationEnabled = radiationEnabledTickBox?.Selected ?? GameMain.NetworkMember.ServerSettings.RadiationEnabled, - MaxMissionCount = maxMissionCount - }; + CampaignSettings settings = elements.CreateSettings(); if (selectedSub.HasTag(SubmarineTag.Shuttle) || !hasRequiredContentPackages) { @@ -172,12 +132,16 @@ namespace Barotrauma }; StartButton.RectTransform.MaxSize = RectTransform.MaxPoint; StartButton.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint); - + InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), buttonContainer.RectTransform), "", font: GUIStyle.SmallFont, textColor: GUIStyle.Green) { TextGetter = () => { - int initialMoney = CampaignMode.InitialMoney; + int initialMoney = 8000; + if (CampaignModePresets.Definitions.TryGetValue(nameof(StartingBalanceAmount).ToIdentifier(), out var definition)) + { + initialMoney = definition.GetInt(elements.StartingFunds.GetValue().ToIdentifier()); + } if (GameMain.NetLobbyScreen.SelectedSub != null) { initialMoney -= GameMain.NetLobbyScreen.SelectedSub.Price; @@ -238,6 +202,7 @@ namespace Barotrauma saveList = new GUIListBox(new RectTransform(Vector2.One, leftColumn.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = SelectSaveFile }; @@ -257,7 +222,7 @@ namespace Barotrauma file1WriteTime = File.GetLastWriteTime(file1); } catch - { + { //do nothing - DateTime.MinValue will be used and the element will get sorted at the bottom of the list }; try diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index 870827d91..40737a179 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -1,12 +1,11 @@ -using Barotrauma.Tutorials; +using Barotrauma.Extensions; +using Barotrauma.IO; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using Barotrauma.IO; +using System.Globalization; using System.Linq; using System.Xml.Linq; -using System.Globalization; -using Barotrauma.Extensions; namespace Barotrauma { @@ -142,7 +141,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUIStyle.SubHeadingFont); seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, ToolBox.RandomSeed(8)); - + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SelectedSub"), font: GUIStyle.SubHeadingFont); var moddedDropdown = new GUIDropDown(new RectTransform(new Vector2(1f, 0.02f), leftColumn.RectTransform), "", 3); @@ -155,8 +154,12 @@ namespace Barotrauma { Stretch = true }; - - subList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.65f), leftColumn.RectTransform)) { ScrollBarVisible = true }; + + subList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.65f), leftColumn.RectTransform)) + { + PlaySoundOnSelect = true, + ScrollBarVisible = 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); var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUIStyle.Font, createClearButton: true); @@ -191,7 +194,7 @@ namespace Barotrauma { TextGetter = () => { - int initialMoney = CampaignMode.InitialMoney; + int initialMoney = CurrentSettings.InitialMoney; if (subList.SelectedData is SubmarineInfo subInfo) { initialMoney -= subInfo.Price; @@ -200,12 +203,16 @@ namespace Barotrauma return TextManager.GetWithVariable("campaignstartingmoney", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", initialMoney)); } }; - + CampaignCustomizeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1f), firstPageButtonContainer.RectTransform, Anchor.CenterLeft), TextManager.Get("SettingsButton")) { OnClicked = (tb, userdata) => { - CreateCustomizeWindow(); + CreateCustomizeWindow(CurrentSettings, settings => + { + CurrentSettings = settings; + UpdateSubList(SubmarineInfo.SavedSubmarines); + }); return true; } }; @@ -218,7 +225,7 @@ namespace Barotrauma return false; } }; - + var disclaimerBtn = new GUIButton(new RectTransform(new Vector2(1.0f, 0.8f), rightColumn.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(5) }, style: "GUINotificationButton") { IgnoreLayoutGroups = true, @@ -353,54 +360,21 @@ namespace Barotrauma StealRandomizeButton(CharacterMenus[i], jobTextContainer); } } - - private void CreateCustomizeWindow() + + private void CreateCustomizeWindow(CampaignSettings prevSettings, Action onClosed = null) { - CampaignCustomizeSettings = new GUIMessageBox("", "", new LocalizedString[] { TextManager.Get("OK") }, new Vector2(0.2f, 0.2f)); - CampaignCustomizeSettings.Buttons[0].OnClicked += CampaignCustomizeSettings.Close; + CampaignCustomizeSettings = new GUIMessageBox("", "", new[] { TextManager.Get("OK") }, new Vector2(0.25f, 0.3f), minSize: new Point(450, 350)); - CampaignSettingsContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), CampaignCustomizeSettings.Content.RectTransform, Anchor.TopCenter)) - { - RelativeSpacing = 0.1f - }; + GUILayoutGroup campaignSettingContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.8f), CampaignCustomizeSettings.Content.RectTransform, Anchor.TopCenter)); - if (MapGenerationParams.Instance.RadiationParams != null) - { - bool prevRadiationToggleEnabled = EnableRadiationToggle?.Selected ?? true; - EnableRadiationToggle = new GUITickBox(new RectTransform(new Vector2(0.3f, 0.3f), CampaignSettingsContent.RectTransform), TextManager.Get("CampaignOption.EnableRadiation"), font: GUIStyle.Font) - { - Selected = prevRadiationToggleEnabled, - ToolTip = TextManager.Get("campaignoption.enableradiation.tooltip") - }; - } - var maxMissionCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), CampaignSettingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - ToolTip = TextManager.Get("maxmissioncounttooltip") - }; - var maxMissionCountDescription = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), maxMissionCountSettingHolder.RectTransform), TextManager.Get("maxmissioncount", "missions"), wrap: true); - var maxMissionCountContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), maxMissionCountSettingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; - var maxMissionCountButtons = new GUIButton[2]; - maxMissionCountButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 0.8f), maxMissionCountContainer.RectTransform), style: "GUIButtonToggleLeft") - { - OnClicked = (button, obj) => - { - MaxMissionCountText.Text = Math.Clamp(Int32.Parse(MaxMissionCountText.Text.SanitizedValue) - 1, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit).ToString(); - return true; - } - }; - RichString prevMaxMissionCountText = MaxMissionCountText?.Text ?? CampaignSettings.DefaultMaxMissionCount.ToString(); - MaxMissionCountText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), maxMissionCountContainer.RectTransform), prevMaxMissionCountText, textAlignment: Alignment.Center, style: "GUITextBox"); - maxMissionCountButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 0.8f), maxMissionCountContainer.RectTransform), style: "GUIButtonToggleRight") + CampaignSettingElements elements = CreateCampaignSettingList(campaignSettingContent, prevSettings); + CampaignCustomizeSettings.Buttons[0].OnClicked += (button, o) => { - OnClicked = (button, obj) => - { - MaxMissionCountText.Text = Math.Clamp(Int32.Parse(MaxMissionCountText.Text.SanitizedValue) + 1, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit).ToString(); - return true; - } + + onClosed?.Invoke(elements.CreateSettings()); + return CampaignCustomizeSettings.Close(button, o); }; - maxMissionCountContainer.Children.ForEach(c => c.ToolTip = maxMissionCountSettingHolder.ToolTip); } private static void StealRandomizeButton(CharacterInfo.AppearanceCustomizationMenu menu, GUIComponent parent) @@ -412,7 +386,7 @@ namespace Barotrauma randomizeButton.RectTransform.Parent = parent.RectTransform; randomizeButton.RectTransform.RelativeSize = Vector2.One * 1.3f; } - + private bool FinishSetup(GUIButton btn, object userdata) { if (string.IsNullOrWhiteSpace(saveNameBox.Text)) @@ -420,7 +394,7 @@ namespace Barotrauma saveNameBox.Flash(GUIStyle.Red); return false; } - + SubmarineInfo selectedSub = null; if (!(subList.SelectedData is SubmarineInfo)) { return false; } @@ -443,16 +417,7 @@ namespace Barotrauma string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Singleplayer, saveNameBox.Text); bool hasRequiredContentPackages = selectedSub.RequiredContentPackagesInstalled; - CampaignSettings settings = new CampaignSettings(); - settings.RadiationEnabled = EnableRadiationToggle?.Selected ?? false; - if (MaxMissionCountText != null && Int32.TryParse(MaxMissionCountText.Text.SanitizedValue, out int missionCount)) - { - settings.MaxMissionCount = missionCount; - } - else - { - settings.MaxMissionCount = CampaignSettings.DefaultMaxMissionCount; - } + CampaignSettings settings = CurrentSettings; if (selectedSub.HasTag(SubmarineTag.Shuttle) || !hasRequiredContentPackages) { @@ -499,7 +464,7 @@ namespace Barotrauma return true; } - + public void RandomizeSeed() { seedBox.Text = ToolBox.RandomSeed(8); @@ -509,7 +474,7 @@ namespace Barotrauma { foreach (GUIComponent child in subList.Content.Children) { - var sub = child.UserData as SubmarineInfo; + SubmarineInfo sub = child.UserData as SubmarineInfo; if (sub == null) { return; } child.Visible = string.IsNullOrEmpty(filter) || sub.DisplayName.Contains(filter.ToLower(), StringComparison.OrdinalIgnoreCase); } @@ -523,7 +488,7 @@ namespace Barotrauma if (!(obj is SubmarineInfo sub)) { return true; } #if !DEBUG - if (sub.Price > CampaignMode.InitialMoney && !GameMain.DebugDraw) + if (sub.Price > CurrentSettings.InitialMoney && !GameMain.DebugDraw) { SetPage(0); nextButton.Enabled = false; @@ -556,8 +521,8 @@ namespace Barotrauma subsToShow.Sort((s1, s2) => { - int p1 = s1.Price > CampaignMode.InitialMoney ? 10 : 0; - int p2 = s2.Price > CampaignMode.InitialMoney ? 10 : 0; + int p1 = s1.Price > CurrentSettings.InitialMoney ? 10 : 0; + int p2 = s2.Price > CurrentSettings.InitialMoney ? 10 : 0; return p1.CompareTo(p2) * 100 + s1.Name.CompareTo(s2.Name); }); @@ -582,13 +547,13 @@ namespace Barotrauma var priceText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), textBlock.RectTransform, Anchor.CenterRight), TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", sub.Price)), textAlignment: Alignment.CenterRight, font: GUIStyle.SmallFont) { - TextColor = sub.Price > CampaignMode.InitialMoney ? GUIStyle.Red : textBlock.TextColor * 0.8f, + TextColor = sub.Price > CurrentSettings.InitialMoney ? GUIStyle.Red : textBlock.TextColor * 0.8f, ToolTip = textBlock.ToolTip }; #if !DEBUG if (!GameMain.DebugDraw) { - if (sub.Price > CampaignMode.InitialMoney || !sub.IsCampaignCompatible) + if (sub.Price > CurrentSettings.InitialMoney || !sub.IsCampaignCompatible) { textBlock.CanBeFocused = false; textBlock.TextColor *= 0.5f; @@ -598,7 +563,7 @@ namespace Barotrauma } if (SubmarineInfo.SavedSubmarines.Any()) { - var validSubs = subsToShow.Where(s => s.IsCampaignCompatible && s.Price <= CampaignMode.InitialMoney).ToList(); + var validSubs = subsToShow.Where(s => s.IsCampaignCompatible && s.Price <= CurrentSettings.InitialMoney).ToList(); if (validSubs.Count > 0) { subList.Select(validSubs[Rand.Int(validSubs.Count)]); @@ -625,6 +590,7 @@ namespace Barotrauma saveList = new GUIListBox(new RectTransform(Vector2.One, leftColumn.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = SelectSaveFile }; @@ -650,8 +616,9 @@ namespace Barotrauma { var saveFrame = CreateSaveElement(saveInfo); if (saveFrame == null) { continue; } - + XDocument doc = SaveUtil.LoadGameSessionDoc(saveInfo.FilePath); + if (doc?.Root == null) { DebugConsole.ThrowError("Error loading save file \"" + saveInfo.FilePath + "\". The file may be corrupted."); @@ -725,9 +692,10 @@ namespace Barotrauma string subName = doc.Root.GetAttributeString("submarine", ""); string saveTime = doc.Root.GetAttributeString("savetime", "unknown"); + DateTime? time = null; if (long.TryParse(saveTime, out long unixTime)) { - DateTime time = ToolBox.Epoch.ToDateTime(unixTime); + time = ToolBox.Epoch.ToDateTime(unixTime); saveTime = time.ToString(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 81973be7f..6cc459e40 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -729,7 +729,7 @@ namespace Barotrauma break; case CampaignMode.InteractionType.PurchaseSub: if (submarineSelection == null) submarineSelection = new SubmarineSelection(false, () => Campaign.ShowCampaignUI = false, tabs[(int)CampaignMode.InteractionType.PurchaseSub].RectTransform); - submarineSelection.RefreshSubmarineDisplay(true); + submarineSelection.RefreshSubmarineDisplay(true, setTransferOptionToTrue: true); break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 72363e2fe..11105e73a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -2858,7 +2858,10 @@ namespace Barotrauma.CharacterEditor { var loadBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadRagdoll"), "", new LocalizedString[] { TextManager.Get("Cancel"), TextManager.Get("Load"), TextManager.Get("Delete") }, messageBoxRelSize); loadBox.Buttons[0].OnClicked += loadBox.Close; - var listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.6f), loadBox.Content.RectTransform, Anchor.TopCenter)); + var listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.6f), loadBox.Content.RectTransform, Anchor.TopCenter)) + { + PlaySoundOnSelect = true, + }; var deleteButton = loadBox.Buttons[2]; deleteButton.Enabled = false; void PopulateListBox() @@ -2996,7 +2999,10 @@ namespace Barotrauma.CharacterEditor { var loadBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadAnimation"), "", new LocalizedString[] { TextManager.Get("Cancel"), TextManager.Get("Load"), TextManager.Get("Delete") }, messageBoxRelSize); loadBox.Buttons[0].OnClicked += loadBox.Close; - var listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.6f), loadBox.Content.RectTransform)); + var listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.6f), loadBox.Content.RectTransform)) + { + PlaySoundOnSelect = true, + }; var deleteButton = loadBox.Buttons[2]; deleteButton.Enabled = false; // Type filtering diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index 3f61ef66d..2f8f73962 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -482,7 +482,10 @@ namespace Barotrauma.CharacterEditor RelativeSpacing = 0.02f }; new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), limbEditLayout.RectTransform), GetCharacterEditorTranslation("Limbs"), font: GUIStyle.SubHeadingFont); - var limbsList = new GUIListBox(new RectTransform(new Vector2(1, 0.45f), content.RectTransform)); + var limbsList = new GUIListBox(new RectTransform(new Vector2(1, 0.45f), content.RectTransform)) + { + PlaySoundOnSelect = true, + }; var removeLimbButton = new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), limbEditLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton") { OnClicked = (b, d) => @@ -659,7 +662,10 @@ namespace Barotrauma.CharacterEditor { CanBeFocused = false }; - var jointsList = new GUIListBox(new RectTransform(new Vector2(1, 0.45f), content.RectTransform)); + var jointsList = new GUIListBox(new RectTransform(new Vector2(1, 0.45f), content.RectTransform)) + { + PlaySoundOnSelect = true, + }; var removeJointButton = new GUIButton(new RectTransform(new Point(jointButtonElement.Rect.Height, jointButtonElement.Rect.Height), jointButtonElement.RectTransform), style: "GUIMinusButton") { OnClicked = (b, d) => diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index 46b2e753f..22b86c510 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -225,7 +225,7 @@ namespace Barotrauma return true; } - public static GUIMessageBox AskForConfirmation(LocalizedString header, LocalizedString body, Func onConfirm) + public static GUIMessageBox AskForConfirmation(LocalizedString header, LocalizedString body, Func onConfirm, GUISoundType? overrideConfirmButtonSound = null) { LocalizedString[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; GUIMessageBox msgBox = new GUIMessageBox(header, body, buttons); @@ -244,6 +244,10 @@ namespace Barotrauma msgBox.Close(); return true; }; + if (overrideConfirmButtonSound.HasValue) + { + msgBox.Buttons[0].ClickSound = overrideConfirmButtonSound.Value; + } return msgBox; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 64a24a84d..ed93ceee0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -34,6 +34,8 @@ namespace Barotrauma private readonly GUITickBox lightingEnabled, cursorLightEnabled, allowInvalidOutpost, mirrorLevel; + private readonly GUIDropDown selectedSubDropDown; + private Sprite editingSprite; private LightSource pointerLightSource; @@ -57,7 +59,10 @@ namespace Barotrauma RelativeSpacing = 0.01f }; - paramsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), paddedLeftPanel.RectTransform)); + paramsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), paddedLeftPanel.RectTransform)) + { + PlaySoundOnSelect = true + }; paramsList.OnSelected += (GUIComponent component, object obj) => { selectedParams = obj as LevelGenerationParams; @@ -70,7 +75,10 @@ namespace Barotrauma var ruinTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.ruinparams"), font: GUIStyle.SubHeadingFont); - ruinParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLeftPanel.RectTransform)); + ruinParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLeftPanel.RectTransform)) + { + PlaySoundOnSelect = true + }; ruinParamsList.OnSelected += (GUIComponent component, object obj) => { CreateOutpostGenerationParamsEditor(obj as OutpostGenerationParams); @@ -79,7 +87,10 @@ namespace Barotrauma var caveTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.caveparams"), font: GUIStyle.SubHeadingFont); - caveParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLeftPanel.RectTransform)); + caveParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLeftPanel.RectTransform)) + { + PlaySoundOnSelect = true + }; caveParamsList.OnSelected += (GUIComponent component, object obj) => { CreateCaveParamsEditor(obj as CaveGenerationParams); @@ -89,7 +100,10 @@ namespace Barotrauma var outpostTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.outpostparams"), font: GUIStyle.SubHeadingFont); GUITextBlock.AutoScaleAndNormalize(ruinTitle, caveTitle, outpostTitle); - outpostParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.2f), paddedLeftPanel.RectTransform)); + outpostParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.2f), paddedLeftPanel.RectTransform)) + { + PlaySoundOnSelect = true + }; outpostParamsList.OnSelected += (GUIComponent component, object obj) => { CreateOutpostGenerationParamsEditor(obj as OutpostGenerationParams); @@ -171,6 +185,16 @@ namespace Barotrauma Vector2 GetSeedElementRelativeSize() => new Vector2(0.5f * (1.0f - randomizeButtonRelativeSize.X), 1.0f); static string GetLevelSeed() => ToolBox.RandomSeed(8); + var subDropDownContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), isHorizontal: true); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), subDropDownContainer.RectTransform), TextManager.Get("submarine")); + selectedSubDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), subDropDownContainer.RectTransform)); + foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) + { + if (sub.Type != SubmarineType.Player) { continue; } + selectedSubDropDown.AddItem(sub.DisplayName, userData: sub); + } + subDropDownContainer.RectTransform.MinSize = new Point(0, selectedSubDropDown.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), @@ -186,11 +210,18 @@ namespace Barotrauma { bool wasLevelLoaded = Level.Loaded != null; Submarine.Unload(); + + if (selectedSubDropDown.SelectedData is SubmarineInfo subInfo) + { + Submarine.MainSub = new Submarine(subInfo); + } GameMain.LightManager.ClearLights(); currentLevelData = LevelData.CreateRandom(seedBox.Text, generationParams: selectedParams); currentLevelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; currentLevelData.AllowInvalidOutpost = allowInvalidOutpost.Selected; - Level.Generate(currentLevelData, mirror: mirrorLevel.Selected); + var dummyLocations = GameSession.CreateDummyLocations(seed: currentLevelData.Seed); + Level.Generate(currentLevelData, mirror: mirrorLevel.Selected, startLocation: dummyLocations[0], endLocation: dummyLocations[1]); + Submarine.MainSub?.SetPosition(Level.Loaded.StartPosition); 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) { @@ -228,7 +259,7 @@ namespace Barotrauma var nonPlayerFiles = ContentPackageManager.EnabledPackages.All.SelectMany(p => p .GetFiles() .Where(f => !(f is SubmarineFile))).ToArray(); - SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == GameSettings.CurrentConfig.QuickStartSub); + SubmarineInfo subInfo = selectedSubDropDown.SelectedData as SubmarineInfo; subInfo ??= SubmarineInfo.SavedSubmarines.GetRandomUnsynced(s => s.IsPlayer && !s.HasTag(SubmarineTag.Shuttle) && !nonPlayerFiles.Any(f => f.Path == s.FilePath)); @@ -259,6 +290,7 @@ namespace Barotrauma levelObjectList = new GUIListBox(new RectTransform(new Vector2(0.99f, 0.85f), bottomPanel.RectTransform, Anchor.Center)) { + PlaySoundOnSelect = true, UseGridLayout = true }; levelObjectList.OnSelected += (GUIComponent component, object obj) => @@ -866,7 +898,11 @@ namespace Barotrauma { foreach (Item item in Item.ItemList) { - item?.GetComponent()?.Update((float)deltaTime, Cam); + if (item == null) { continue; } + foreach (var light in item.GetComponents()) + { + light.Update((float)deltaTime, Cam); + } } } GameMain.LightManager?.Update((float)deltaTime); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index f8c3d29c9..14748954f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -429,7 +429,10 @@ namespace Barotrauma //PLACEHOLDER var tutorialList = new GUIListBox( - new RectTransform(new Vector2(0.95f, 0.85f), menuTabs[Tab.Tutorials].RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.1f) }); + new RectTransform(new Vector2(0.95f, 0.85f), menuTabs[Tab.Tutorials].RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.1f) }) + { + PlaySoundOnSelect = true, + }; var tutorialTypes = new List() { typeof(MechanicTutorial), @@ -850,16 +853,12 @@ namespace Barotrauma arguments += " -nopassword"; } - int ownerKey = 0; if (Steam.SteamManager.GetSteamID() != 0) { arguments += " -steamid " + Steam.SteamManager.GetSteamID(); } - else - { - ownerKey = Math.Max(CryptoRandom.Instance.Next(), 1); - arguments += " -ownerkey " + ownerKey; - } + int ownerKey = Math.Max(CryptoRandom.Instance.Next(), 1); + arguments += " -ownerkey " + ownerKey; string filename = Path.Combine( Path.GetDirectoryName(exeName), @@ -1244,7 +1243,8 @@ namespace Barotrauma new GUIButton(new RectTransform(Vector2.One, buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton", textAlignment: Alignment.Center) { UserData = -1, - OnClicked = ChangeMaxPlayers + OnClicked = ChangeMaxPlayers, + ClickSound = GUISoundType.Decrease }; maxPlayersBox = new GUITextBox(new RectTransform(new Vector2(0.6f, 1.0f), buttonContainer.RectTransform), textAlignment: Alignment.Center) { @@ -1264,7 +1264,8 @@ namespace Barotrauma new GUIButton(new RectTransform(Vector2.One, buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton", textAlignment: Alignment.Center) { UserData = 1, - OnClicked = ChangeMaxPlayers + OnClicked = ChangeMaxPlayers, + ClickSound = GUISoundType.Increase }; maxPlayersLabel.RectTransform.IsFixedSize = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 001618db3..1c21c6f27 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -179,7 +179,7 @@ namespace Barotrauma get { return ModeList.SelectedIndex; } set { - ModeList.Select(value, true); + ModeList.Select(value, GUIListBox.Force.Yes); } } @@ -504,6 +504,7 @@ namespace Barotrauma PlayerList = new GUIListBox(new RectTransform(new Vector2(0.4f, 1.0f), socialHolderHorizontal.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = (component, userdata) => { SelectPlayer(userdata as Client); return true; } }; @@ -816,6 +817,7 @@ namespace Barotrauma SubList = new GUIListBox(new RectTransform(Vector2.One, subHolder.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = VotableClicked }; @@ -901,6 +903,7 @@ namespace Barotrauma }; ModeList = new GUIListBox(new RectTransform(Vector2.One, gameModeHolder.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = VotableClicked }; @@ -1515,6 +1518,7 @@ namespace Barotrauma JobList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.6f), JobPreferenceContainer.RectTransform, Anchor.BottomCenter), true) { Enabled = true, + PlaySoundOnSelect = true, OnSelected = (child, obj) => { if (child.IsParentOf(GUI.MouseOn)) return false; @@ -1600,6 +1604,7 @@ namespace Barotrauma { Enabled = true, KeepSpaceForScrollBar = false, + PlaySoundOnSelect = true, ScrollBarEnabled = false, ScrollBarVisible = false }; @@ -3185,7 +3190,7 @@ namespace Barotrauma var prevMode = ModeList.Content.GetChild(selectedModeIndex).UserData as GameModePreset; - if ((HighlightedModeIndex == selectedModeIndex || HighlightedModeIndex < 0) && ModeList.SelectedIndex != modeIndex) { ModeList.Select(modeIndex, true); } + if ((HighlightedModeIndex == selectedModeIndex || HighlightedModeIndex < 0) && ModeList.SelectedIndex != modeIndex) { ModeList.Select(modeIndex, GUIListBox.Force.Yes); } selectedModeIndex = modeIndex; if ((prevMode == GameModePreset.PvP) != (SelectedMode == GameModePreset.PvP)) @@ -3301,7 +3306,7 @@ namespace Barotrauma RefreshEnabledElements(); if (enabled) { - ModeList.Select(GameModePreset.MultiPlayerCampaign, true); + ModeList.Select(GameModePreset.MultiPlayerCampaign, GUIListBox.Force.Yes); } } @@ -3417,7 +3422,7 @@ namespace Barotrauma UserData = i, OnClicked = (btn, obj) => { - JobList.Select((int)obj, true); + JobList.Select((int)obj, GUIListBox.Force.Yes); SwitchJob(btn, null); if (JobSelectionFrame != null) { JobSelectionFrame.Visible = false; } JobList.Deselect(); @@ -3553,7 +3558,7 @@ namespace Barotrauma else { subList.OnSelected -= VotableClicked; - subList.Select(sub, force: true); + subList.Select(sub, GUIListBox.Force.Yes); subList.OnSelected += VotableClicked; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs index 81473f6d6..c48aa0ac5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs @@ -129,7 +129,10 @@ namespace Barotrauma OnClicked = (btn, userdata) => { FilterEmitters(""); filterBox.Text = ""; filterBox.Flash(Color.White); return true; } }; - prefabList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), paddedLeftPanel.RectTransform)); + prefabList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), paddedLeftPanel.RectTransform)) + { + PlaySoundOnSelect = true, + }; prefabList.OnSelected += (GUIComponent component, object obj) => { cam.Position = Vector2.Zero; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs index 1bf91b69e..49bed19f7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -504,6 +504,7 @@ namespace Barotrauma serverList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), serverListContainer.RectTransform, Anchor.Center)) { + PlaySoundOnSelect = true, ScrollBarVisible = true, OnSelected = (btn, obj) => { @@ -1473,7 +1474,6 @@ namespace Barotrauma { friendsDropdownButton = new GUIButton(new RectTransform(Vector2.One, friendsButtonHolder.RectTransform, Anchor.BottomRight, Pivot.BottomRight, scaleBasis: ScaleBasis.BothHeight), "\u2022 \u2022 \u2022", style: "GUIButtonFriendsDropdown") { - Font = GUIStyle.GlobalFont, OnClicked = (button, udt) => { friendsDropdown.RectTransform.NonScaledSize = new Point(friendsButtonHolder.Rect.Height * 5 * 166 / 100, friendsButtonHolder.Rect.Height * 4 * 166 / 100); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index 8a78c4e45..df8fc28ba 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -85,12 +85,12 @@ namespace Barotrauma { OnClicked = (button, userData) => { - var selected = selectedSprites; + var selected = selectedSprites.ToList(); Sprite firstSelected = selected.First(); selected.ForEach(s => s.ReloadTexture()); RefreshLists(); - textureList.Select(firstSelected.FullPath, autoScroll: false); - selected.ForEachMod(s => spriteList.Select(s, autoScroll: false)); + textureList.Select(firstSelected.FullPath, autoScroll: GUIListBox.AutoScroll.Disabled); + selected.ForEachMod(s => spriteList.Select(s, autoScroll: GUIListBox.AutoScroll.Disabled)); texturePathText.Text = TextManager.GetWithVariable("spriteeditor.texturesreloaded", "[filepath]", firstSelected.FilePath.Value); texturePathText.TextColor = GUIStyle.Green; return true; @@ -206,6 +206,7 @@ namespace Barotrauma textureList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), paddedLeftPanel.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = (listBox, userData) => { var newTexturePath = userData as string; @@ -213,7 +214,7 @@ namespace Barotrauma { selectedTexturePath = newTexturePath; ResetZoom(); - spriteList.Select(loadedSprites.First(s => s.FilePath == selectedTexturePath), autoScroll: false); + spriteList.Select(loadedSprites.First(s => s.FilePath == selectedTexturePath), autoScroll: GUIListBox.AutoScroll.Disabled); UpdateScrollBar(spriteList); } foreach (GUIComponent child in spriteList.Content.Children) @@ -248,6 +249,7 @@ namespace Barotrauma spriteList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), paddedRightPanel.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = (listBox, userData) => { if (userData is Sprite sprite) @@ -481,7 +483,7 @@ namespace Barotrauma var scaledRect = new Rectangle(textureRect.Location + sprite.SourceRect.Location.Multiply(zoom), sprite.SourceRect.Size.Multiply(zoom)); if (scaledRect.Contains(PlayerInput.MousePosition)) { - spriteList.Select(sprite, autoScroll: false); + spriteList.Select(sprite, autoScroll: GUIListBox.AutoScroll.Disabled); UpdateScrollBar(spriteList); UpdateScrollBar(textureList); // Release the keyboard so that we can nudge the source rects @@ -847,7 +849,7 @@ namespace Barotrauma base.Select(); LoadSprites(); RefreshLists(); - spriteList.Select(0, autoScroll: false); + spriteList.Select(0, autoScroll: GUIListBox.AutoScroll.Disabled); } protected override void DeselectEditorSpecific() @@ -905,7 +907,7 @@ namespace Barotrauma } if (sprite.FullPath != selectedTexturePath) { - textureList.Select(sprite.FullPath, autoScroll: false); + textureList.Select(sprite.FullPath, autoScroll: GUIListBox.AutoScroll.Disabled); UpdateScrollBar(textureList); } xmlPathText.Text = string.Empty; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index b4c90e032..a702a3d4d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Xml.Linq; using Microsoft.Xna.Framework.Input; using Barotrauma.IO; +using Barotrauma.Steam; namespace Barotrauma { @@ -58,15 +59,6 @@ namespace Barotrauma islinked = Linkage; } } - - #warning TODO: switch this to an enum? - private static readonly ImmutableArray crewExperienceLevels = new string[] - { - "CrewExperienceLow", - "CrewExperienceMid", - "CrewExperienceHigh" - }.ToImmutableArray(); - public enum Mode { @@ -99,6 +91,8 @@ namespace Barotrauma private SubmarineInfo backedUpSubInfo; + private readonly HashSet publishedWorkshopItemIds = new HashSet(); + private Point screenResolution; private bool lightingEnabled; @@ -550,6 +544,7 @@ namespace Barotrauma }; previouslyUsedList = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.9f), previouslyUsedPanel.RectTransform, Anchor.Center)) { + PlaySoundOnSelect = true, ScrollBarVisible = true, OnSelected = SelectPrefab }; @@ -635,6 +630,7 @@ namespace Barotrauma undoBufferList = new GUIListBox(new RectTransform(subPanelSize, undoBufferPanel.RectTransform, Anchor.Center)) { + PlaySoundOnSelect = true, ScrollBarVisible = true, OnSelected = (_, userData) => { @@ -1006,7 +1002,8 @@ namespace Barotrauma OnSelected = SelectPrefab, UseGridLayout = true, CheckSelected = MapEntityPrefab.GetSelected, - Visible = false + Visible = false, + PlaySoundOnSelect = true, }; paddedTab.Recalculate(); @@ -1136,7 +1133,8 @@ namespace Barotrauma OnSelected = SelectPrefab, UseGridLayout = true, CheckSelected = MapEntityPrefab.GetSelected, - ClampMouseRectToParent = true + ClampMouseRectToParent = true, + PlaySoundOnSelect = true, }; entityListInner.ContentBackground.ClampMouseRectToParent = true; entityListInner.Content.ClampMouseRectToParent = true; @@ -1190,7 +1188,7 @@ 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 = $"{frame.ToolTip}\n‖color:{XMLExtensions.ToStringHex(GUIStyle.TextColorBright)}‖{name}‖color:end‖"; + frame.ToolTip = $"‖color:{XMLExtensions.ToStringHex(GUIStyle.TextColorBright)}‖{name}‖color:end‖"; if (!ep.Description.IsNullOrEmpty()) { frame.ToolTip += '\n' + ep.Description; @@ -1326,6 +1324,17 @@ namespace Barotrauma { base.Select(); + TaskPool.Add( + $"DeterminePublishedItemIds", + SteamManager.Workshop.GetPublishedItems(), + t => + { + if (!t.TryGetResult(out ISet items)) { return; } + + publishedWorkshopItemIds.Clear(); + publishedWorkshopItemIds.UnionWith(items.Select(it => it.Id.Value)); + }); + GUI.PreventPauseMenuToggle = false; if (!Directory.Exists(autoSavePath)) { @@ -1573,10 +1582,6 @@ namespace Barotrauma ClearFilter(); ClearLayers(); - while (packageReloadQueue.TryDequeue(out var p)) - { - ContentPackageManager.ReloadContentPackage(p); - } } private void CreateDummyCharacter() @@ -1699,8 +1704,20 @@ namespace Barotrauma autoSaveLabel?.FadeOut(0.5f, true, 1f); } - private bool SaveSub(GUIButton button, object obj) + private bool SaveSub(ContentPackage packageToSaveTo) { + void handleExceptions(Action action) + { + try + { + action(); + } + catch (Exception e) + { + DebugConsole.ThrowError($"An error occurred while trying to save {nameBox.Text}", e, createMessageBox: true); + } + } + if (string.IsNullOrWhiteSpace(nameBox.Text)) { GUI.AddMessage(TextManager.Get("SubNameMissingWarning"), GUIStyle.Red); @@ -1722,7 +1739,7 @@ namespace Barotrauma msgBox.Buttons[0].OnClicked = (bt, userdata) => { - SaveSubToFile(nameBox.Text); + handleExceptions(() => SaveSubToFile(nameBox.Text, packageToSaveTo)); saveFrame = null; msgBox.Close(); return true; @@ -1735,17 +1752,22 @@ namespace Barotrauma return true; } - var result = SaveSubToFile(nameBox.Text); + bool result = false; + handleExceptions(() => result = SaveSubToFile(nameBox.Text, packageToSaveTo)); saveFrame = null; return result; } - private readonly Queue packageReloadQueue = new Queue(); - - private void EnqueueForReload(ContentPackage p) + private void ReloadModifiedPackage(ContentPackage p) { if (p is null) { return; } - if (!packageReloadQueue.Contains(p)) { packageReloadQueue.Enqueue(p); } + p.ReloadSubsAndItemAssemblies(); + if (p.Files.Length == 0) + { + Directory.Delete(p.Dir, recursive: true); + ContentPackageManager.LocalPackages.Refresh(); + ContentPackageManager.EnabledPackages.DisableRemovedMods(); + } } public static Type DetermineSubFileType(SubmarineType type) @@ -1760,12 +1782,9 @@ namespace Barotrauma SubmarineType.Player => typeof(SubmarineFile), _ => null }; - - private bool SaveSubToFile(string name) - { - bool canModifyPackage(ContentPackage p) - => p != null && ContentPackageManager.LocalPackages.Contains(p) && p != ContentPackageManager.VanillaCorePackage; + private bool SaveSubToFile(string name, ContentPackage packageToSaveTo) + { Type subFileType = DetermineSubFileType(MainSub?.Info.Type ?? SubmarineType.Player); void addSubAndSaveModProject(ModProject modProject, string filePath, string packagePath) @@ -1784,6 +1803,7 @@ namespace Barotrauma modProject.AddFile(newFile); } + using var _ = Validation.SkipInDebugBuilds(); modProject.DiscardHashAndInstallTime(); modProject.Save(packagePath); } @@ -1819,61 +1839,66 @@ namespace Barotrauma name = name.Trim(); string newLocalModDir = $"{ContentPackage.LocalModsDir}/{name}"; - - var vanilla = GameMain.VanillaContent; - var vanillaSubs = vanilla?.GetFiles()?.Select(f => f.Path); - bool isVanillaSub = vanillaSubs?.Any(f => f.Value == MainSub.Info.FilePath.CleanUpPath()) ?? false; - string savePath = name + ".sub"; + string savePath = $"{name}.sub"; string prevSavePath = null; - if (!string.IsNullOrEmpty(MainSub?.Info.FilePath) && - MainSub.Info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) + if (packageToSaveTo != null) + { + var modProject = new ModProject(packageToSaveTo); + var fileListPath = packageToSaveTo.Path; + if (packageToSaveTo == ContentPackageManager.VanillaCorePackage) + { +#if !DEBUG + throw new InvalidOperationException("Cannot save to Vanilla package"); +#endif + savePath = string.Format((MainSub?.Info.Type ?? SubmarineType.Player) switch + { + SubmarineType.Player => "Content/Submarines/{0}", + SubmarineType.Outpost => "Content/Map/Outposts/{0}", + SubmarineType.Ruin => "Content/Submarines/{0}", //we don't seem to use this anymore... + SubmarineType.Wreck => "Content/Map/Wrecks/{0}", + SubmarineType.BeaconStation => "Content/Map/BeaconStations/{0}", + SubmarineType.EnemySubmarine => "Content/Map/EnemySubmarines/{0}", + SubmarineType.OutpostModule => "Content/Map/Outposts/{0}", + _ => throw new InvalidOperationException() + }, savePath); + modProject.ModVersion = ""; + } + else + { + savePath = Path.Combine(packageToSaveTo.Dir, savePath); + } + addSubAndSaveModProject(modProject, savePath, fileListPath); + } + else if (MainSub?.Info != null + && MainSub.Info.FilePath.StartsWith(ContentPackage.LocalModsDir) + && MainSub.Info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) { prevSavePath = MainSub.Info.FilePath.CleanUpPath(); string prevDir = Path.GetDirectoryName(MainSub.Info.FilePath).CleanUpPath(); - string[] subDirs = prevDir.Split('/'); - ModProject modProject = new ModProject() { Name = name }; + ModProject modProject = new ModProject { Name = name }; string fileListPath = null; - if (subDirs.Length > 1 && subDirs[0].Equals(ContentPackage.LocalModsDir, StringComparison.InvariantCultureIgnoreCase)) + ContentPackage contentPackage = GetLocalPackageThatOwnsSub(MainSub.Info); + if (contentPackage != null) { - string modName = subDirs[1]; - ContentPackage contentPackage = ContentPackageManager.EnabledPackages.All.FirstOrDefault(p => p.Name.Equals(modName, StringComparison.InvariantCultureIgnoreCase)); - if (contentPackage != null) - { - modProject = new ModProject(contentPackage); - fileListPath = contentPackage.Path; - EnqueueForReload(contentPackage); - } + modProject = new ModProject(contentPackage); + fileListPath = contentPackage.Path; + packageToSaveTo = contentPackage; } - savePath = Path.Combine(prevDir, savePath).CleanUpPath(); - if (!isVanillaSub) - { - addSubAndSaveModProject(modProject, savePath, fileListPath ?? Path.Combine(Path.GetDirectoryName(savePath), ContentPackage.FileListFileName)); - } + savePath = Path.Combine(prevDir, savePath).CleanUpPath(); + addSubAndSaveModProject(modProject, savePath, fileListPath ?? Path.Combine(Path.GetDirectoryName(savePath), ContentPackage.FileListFileName)); } else { - savePath = Path.Combine(newLocalModDir, savePath); - ModProject modProject = new ModProject() { Name = name }; + savePath = Path.Combine(newLocalModDir, savePath); + ModProject modProject = new ModProject { Name = name }; addSubAndSaveModProject(modProject, savePath, Path.Combine(Path.GetDirectoryName(savePath), ContentPackage.FileListFileName)); } savePath = savePath.CleanUpPathCrossPlatform(correctFilenameCase: false); -#if !DEBUG - if (vanilla != null) - { - string pathToCompare = savePath.Replace(@"\", @"/"); - if (vanillaSubs.Any(sub => sub.Value.Replace(@"\", @"/").Equals(pathToCompare, StringComparison.OrdinalIgnoreCase))) - { - GUI.AddMessage(TextManager.Get("CannotEditVanillaSubs"), GUIStyle.Red, font: GUIStyle.LargeFont); - return false; - } - } -#endif - if (MainSub != null) { Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; @@ -1912,6 +1937,7 @@ namespace Barotrauma GameSettings.SaveCurrentConfig(); } } + if (packageToSaveTo != null) { ReloadModifiedPackage(packageToSaveTo); } SubmarineInfo.RefreshSavedSub(savePath); if (prevSavePath != null && prevSavePath != savePath) { SubmarineInfo.RefreshSavedSub(prevSavePath); } MainSub.Info.PreviewImage = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.FilePath == savePath)?.PreviewImage; @@ -1940,12 +1966,7 @@ namespace Barotrauma SetMode(Mode.Default); } - saveFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) - { - OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) saveFrame = null; return true; } - }; - - new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, saveFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + 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 paddedSaveFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.02f }; @@ -1964,7 +1985,7 @@ namespace Barotrauma submarineNameCharacterCount = new GUITextBlock(new RectTransform(new Vector2(.5f, 1f), nameHeaderGroup.RectTransform), string.Empty, textAlignment: Alignment.TopRight); - nameBox = new GUITextBox(new RectTransform(new Vector2(.95f, 0.05f), leftColumn.RectTransform)) + nameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform)) { OnEnterPressed = ChangeSubName }; @@ -2037,17 +2058,14 @@ namespace Barotrauma //--------------------------------------- - var outpostSettingsContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), leftColumn.RectTransform)) + var subTypeDependentSettingFrame = new GUIFrame(new RectTransform((1.0f, 0.5f), leftColumn.RectTransform), style: "InnerFrame"); + + var outpostSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, subTypeDependentSettingFrame.RectTransform)) { - IgnoreLayoutGroups = true, CanBeFocused = true, Visible = false, Stretch = true }; - new GUIFrame(new RectTransform(Vector2.One, outpostSettingsContainer.RectTransform), "InnerFrame") - { - IgnoreLayoutGroups = true - }; // module flags --------------------- @@ -2286,15 +2304,75 @@ namespace Barotrauma }; outpostSettingsContainer.RectTransform.MinSize = new Point(0, outpostSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0)); - //------------------------------------------------------------------ + //--------------------------------------- - var subSettingsContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), leftColumn.RectTransform)) + var beaconSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, subTypeDependentSettingFrame.RectTransform)) + { + CanBeFocused = true, + Visible = false, + Stretch = true + }; + + // ------------------- + + var beaconMinDifficultyGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), isHorizontal: true) { Stretch = true }; - new GUIFrame(new RectTransform(Vector2.One, subSettingsContainer.RectTransform), "InnerFrame") + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), beaconMinDifficultyGroup.RectTransform), + TextManager.Get("minleveldifficulty"), textAlignment: Alignment.CenterLeft, wrap: true); + new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), beaconMinDifficultyGroup.RectTransform), NumberType.Int) { - IgnoreLayoutGroups = true + IntValue = (int)(MainSub?.Info?.BeaconStationInfo?.MinLevelDifficulty ?? 0), + MinValueInt = 0, + MaxValueInt = 100, + OnValueChanged = (numberInput) => + { + MainSub.Info.BeaconStationInfo.MinLevelDifficulty = numberInput.IntValue; + } + }; + var beaconMaxDifficultyGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), isHorizontal: true) + { + Stretch = true + }; + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), beaconMaxDifficultyGroup.RectTransform), + TextManager.Get("maxleveldifficulty"), textAlignment: Alignment.CenterLeft, wrap: true); + new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), beaconMaxDifficultyGroup.RectTransform), NumberType.Int) + { + IntValue = (int)(MainSub?.Info?.BeaconStationInfo?.MaxLevelDifficulty ?? 100), + MinValueInt = 0, + MaxValueInt = 100, + OnValueChanged = (numberInput) => + { + MainSub.Info.BeaconStationInfo.MaxLevelDifficulty = numberInput.IntValue; + } + }; + + new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), TextManager.Get("allowdamagedwalls")) + { + Selected = MainSub?.Info?.BeaconStationInfo?.AllowDamagedWalls ?? true, + OnSelected = (tb) => + { + MainSub.Info.BeaconStationInfo.AllowDamagedWalls = tb.Selected; + return true; + } + }; + new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), TextManager.Get("allowdisconnectedwires")) + { + Selected = MainSub?.Info?.BeaconStationInfo?.AllowDisconnectedWires ?? true, + OnSelected = (tb) => + { + MainSub.Info.BeaconStationInfo.AllowDisconnectedWires = tb.Selected; + return true; + } + }; + beaconSettingsContainer.RectTransform.MinSize = new Point(0, beaconSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0)); + + //------------------------------------------------------------------ + + var subSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, subTypeDependentSettingFrame.RectTransform)) + { + Stretch = true }; var priceGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) @@ -2321,7 +2399,7 @@ namespace Barotrauma MainSub.Info.Price = Math.Max(MainSub.Info.Price, basePrice); } - var classGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) + var classGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; @@ -2329,19 +2407,27 @@ namespace Barotrauma TextManager.Get("submarineclass"), textAlignment: Alignment.CenterLeft, wrap: true); GUIDropDown classDropDown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1.0f), classGroup.RectTransform)); classDropDown.RectTransform.MinSize = new Point(0, subTypeContainer.RectTransform.Children.Max(c => c.MinSize.Y)); - classDropDown.AddItem(TextManager.Get("submarineclass.undefined"), SubmarineClass.Undefined); - classDropDown.AddItem(TextManager.Get("submarineclass.scout"), SubmarineClass.Scout); - classDropDown.AddItem(TextManager.Get("submarineclass.attack"), SubmarineClass.Attack); - classDropDown.AddItem(TextManager.Get("submarineclass.transport"), SubmarineClass.Transport); - classDropDown.AddItem(TextManager.Get("submarineclass.deepdiver"), SubmarineClass.DeepDiver); + foreach (SubmarineClass @class in Enum.GetValues(typeof(SubmarineClass))) + { + classDropDown.AddItem(TextManager.Get($"{nameof(SubmarineClass)}.{@class}"), @class); + } + classDropDown.AddItem(TextManager.Get(nameof(SubmarineTag.Shuttle)), SubmarineTag.Shuttle); classDropDown.OnSelected += (selected, userdata) => { - SubmarineClass submarineClass = (SubmarineClass)userdata; - MainSub.Info.SubmarineClass = submarineClass; + switch (userdata) + { + case SubmarineClass submarineClass: + MainSub.Info.RemoveTag(SubmarineTag.Shuttle); + MainSub.Info.SubmarineClass = submarineClass; + break; + case SubmarineTag.Shuttle: + MainSub.Info.AddTag(SubmarineTag.Shuttle); + MainSub.Info.SubmarineClass = SubmarineClass.Undefined; + break; + } return true; }; - classDropDown.SelectItem(MainSub.Info.SubmarineClass); - classText.Enabled = classDropDown.ButtonEnabled = !MainSub.Info.HasTag(SubmarineTag.Shuttle); + classDropDown.SelectItem(!MainSub.Info.HasTag(SubmarineTag.Shuttle) ? MainSub.Info.SubmarineClass : (object)SubmarineTag.Shuttle); var crewSizeArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) { @@ -2388,30 +2474,55 @@ namespace Barotrauma var toggleExpLeft = new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), crewExpArea.RectTransform), style: "GUIButtonToggleLeft"); var experienceText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), crewExpArea.RectTransform), - text: crewExperienceLevels[0], textAlignment: Alignment.Center); + text: TextManager.Get(SubmarineInfo.CrewExperienceLevel.CrewExperienceLow.ToIdentifier()), textAlignment: Alignment.Center); var toggleExpRight = new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), crewExpArea.RectTransform), style: "GUIButtonToggleRight"); toggleExpLeft.OnClicked += (btn, userData) => { - int currentIndex = crewExperienceLevels.IndexOf((string)experienceText.UserData); - currentIndex--; - if (currentIndex < 0) currentIndex = crewExperienceLevels.Length - 1; - experienceText.UserData = crewExperienceLevels[currentIndex]; - experienceText.Text = TextManager.Get(crewExperienceLevels[currentIndex]); - MainSub.Info.RecommendedCrewExperience = (string)experienceText.UserData; + MainSub.Info.RecommendedCrewExperience--; + if (MainSub.Info.RecommendedCrewExperience < SubmarineInfo.CrewExperienceLevel.CrewExperienceLow) + { + MainSub.Info.RecommendedCrewExperience = SubmarineInfo.CrewExperienceLevel.CrewExperienceHigh; + } + experienceText.Text = TextManager.Get(MainSub.Info.RecommendedCrewExperience.ToIdentifier()); return true; }; toggleExpRight.OnClicked += (btn, userData) => { - int currentIndex = crewExperienceLevels.IndexOf((string)experienceText.UserData); - currentIndex++; - if (currentIndex >= crewExperienceLevels.Length) currentIndex = 0; - experienceText.UserData = crewExperienceLevels[currentIndex]; - experienceText.Text = TextManager.Get(crewExperienceLevels[currentIndex]); - MainSub.Info.RecommendedCrewExperience = (string)experienceText.UserData; + MainSub.Info.RecommendedCrewExperience++; + if (MainSub.Info.RecommendedCrewExperience > SubmarineInfo.CrewExperienceLevel.CrewExperienceHigh) + { + MainSub.Info.RecommendedCrewExperience = SubmarineInfo.CrewExperienceLevel.CrewExperienceLow; + } + experienceText.Text = TextManager.Get(MainSub.Info.RecommendedCrewExperience.ToIdentifier()); return true; }; + + var hideInMenusArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + AbsoluteSpacing = 5 + }; + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), hideInMenusArea.RectTransform), + TextManager.Get("HideInMenus"), textAlignment: Alignment.CenterLeft, wrap: true, font: GUIStyle.SmallFont); + + new GUITickBox(new RectTransform((0.4f, 1.0f), hideInMenusArea.RectTransform), "") + { + Selected = MainSub.Info.HasTag(SubmarineTag.HideInMenus), + OnSelected = box => + { + if (box.Selected) + { + MainSub.Info.AddTag(SubmarineTag.HideInMenus); + } + else + { + MainSub.Info.RemoveTag(SubmarineTag.HideInMenus); + } + return true; + } + }; if (MainSub != null) { @@ -2419,9 +2530,11 @@ namespace Barotrauma int max = MainSub.Info.RecommendedCrewSizeMax; crewSizeMin.IntValue = min; crewSizeMax.IntValue = max; - experienceText.UserData = string.IsNullOrEmpty(MainSub.Info.RecommendedCrewExperience) ? - crewExperienceLevels[0] : MainSub.Info.RecommendedCrewExperience; - experienceText.Text = TextManager.Get((string)experienceText.UserData); + if (MainSub.Info.RecommendedCrewExperience == SubmarineInfo.CrewExperienceLevel.Unknown) + { + MainSub.Info.RecommendedCrewExperience = SubmarineInfo.CrewExperienceLevel.CrewExperienceLow; + } + experienceText.Text = TextManager.Get(MainSub.Info.RecommendedCrewExperience.ToIdentifier()); } subTypeDropdown.OnSelected += (selected, userdata) => @@ -2434,10 +2547,10 @@ namespace Barotrauma } previewImageButtonHolder.Children.ForEach(c => c.Enabled = type != SubmarineType.OutpostModule); outpostSettingsContainer.Visible = type == SubmarineType.OutpostModule; - outpostSettingsContainer.IgnoreLayoutGroups = !outpostSettingsContainer.Visible; + + beaconSettingsContainer.Visible = type == SubmarineType.BeaconStation; subSettingsContainer.Visible = type == SubmarineType.Player; - subSettingsContainer.IgnoreLayoutGroups = !subSettingsContainer.Visible; return true; }; subSettingsContainer.RectTransform.MinSize = new Point(0, subSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0)); @@ -2498,77 +2611,144 @@ namespace Barotrauma previewImageButtonHolder.RectTransform.MinSize = new Point(0, previewImageButtonHolder.RectTransform.Children.Max(c => c.MinSize.Y)); - var horizontalArea = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.45f), rightColumn.RectTransform), style: null); + var contentPackageTabber = new GUILayoutGroup(new RectTransform((1.0f, 0.06f), rightColumn.RectTransform), isHorizontal: true); - var settingsLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), horizontalArea.RectTransform), - TextManager.Get("SaveSubDialogSettings"), wrap: true, font: GUIStyle.SmallFont); - - var tagContainer = new GUIListBox(new RectTransform(new Vector2(0.5f, 1.0f - settingsLabel.RectTransform.RelativeSize.Y), - horizontalArea.RectTransform, Anchor.BottomLeft), - style: "InnerFrame"); - - foreach (SubmarineTag tag in Enum.GetValues(typeof(SubmarineTag))) + GUIButton createTabberBtn(string labelTag) { - LocalizedString tagStr = TextManager.Get(tag.ToString()); - var tagTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), tagContainer.Content.RectTransform), - tagStr, font: GUIStyle.SmallFont) - { - Selected = MainSub != null && MainSub.Info.HasTag(tag), - UserData = tag, - OnSelected = (GUITickBox tickBox) => - { - if (MainSub == null) return false; - SubmarineTag tag = (SubmarineTag)tickBox.UserData; - if (tag == SubmarineTag.Shuttle) - { - if (tickBox.Selected) - { - classDropDown.SelectItem(SubmarineClass.Undefined); - } - else - { - classDropDown.SelectItem(MainSub.Info.SubmarineClass); - } - classText.Enabled = classDropDown.ButtonEnabled = !tickBox.Selected; - } - if (tickBox.Selected) - { - MainSub.Info.AddTag(tag); - } - else - { - MainSub.Info.RemoveTag(tag); - } - return true; - } - }; + var btn = new GUIButton(new RectTransform((0.5f, 1.0f), contentPackageTabber.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter), TextManager.Get(labelTag), style: "GUITabButton"); + btn.RectTransform.MaxSize = RectTransform.MaxPoint; + btn.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint); + btn.Font = GUIStyle.SmallFont; + return btn; } - var contentPackagesLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), + var saveToPackageTabBtn = createTabberBtn("SaveToLocalPackage"); + saveToPackageTabBtn.Selected = true; + var reqPackagesTabBtn = createTabberBtn("RequiredContentPackages"); + reqPackagesTabBtn.Selected = false; + + var horizontalArea = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.45f), rightColumn.RectTransform), style: null); + + var saveInPackageLayout = new GUILayoutGroup(new RectTransform(Vector2.One, horizontalArea.RectTransform, Anchor.BottomRight)) { Stretch = true }; - var contentPackagesLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), contentPackagesLayout.RectTransform), - TextManager.Get("RequiredContentPackages"), wrap: true, font: GUIStyle.SmallFont); - contentPackagesLabel.RectTransform.MinSize - = GUIStyle.SmallFont.MeasureString(contentPackagesLabel.WrappedText).ToPoint(); + var packageToSaveInList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), + saveInPackageLayout.RectTransform)); - var contentPackList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), - contentPackagesLayout.RectTransform)); + var packToSaveInFilter + = new GUITextBox(new RectTransform((1.0f, 0.15f), saveInPackageLayout.RectTransform), + createClearButton: true); + GUILayoutGroup addItemToPackageToSaveList(LocalizedString itemText, ContentPackage p) + { + var listItem = new GUIFrame(new RectTransform((1.0f, 0.15f), packageToSaveInList.Content.RectTransform), + style: "ListBoxElement") + { + UserData = p + }; + if (p != null && p != ContentPackageManager.VanillaCorePackage) { listItem.ToolTip = p.Dir; } + var retVal = + new GUILayoutGroup(new RectTransform(Vector2.One, listItem.RectTransform), + isHorizontal: true) { Stretch = true }; + var iconFrame = + new GUIFrame( + new RectTransform(Vector2.One, retVal.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: null) { CanBeFocused = false }; + var pkgText = new GUITextBlock(new RectTransform(Vector2.One, retVal.RectTransform), itemText) + { CanBeFocused = false }; + return retVal; + } + +#if DEBUG + //this is a debug-only option so I won't bother submitting it for localization + var modifyVanillaListItem = addItemToPackageToSaveList("Modify Vanilla content package", ContentPackageManager.VanillaCorePackage); + var modifyVanillaListIcon = modifyVanillaListItem.GetChild(); + GUIStyle.Apply(modifyVanillaListIcon, "WorkshopMenu.EditButton"); +#endif + + var newPackageListItem = addItemToPackageToSaveList(TextManager.Get("CreateNewLocalPackage"), null); + var newPackageListIcon = newPackageListItem.GetChild(); + var newPackageListText = newPackageListItem.GetChild(); + GUIStyle.Apply(newPackageListIcon, "NewContentPackageIcon"); + new GUICustomComponent(new RectTransform(Vector2.Zero, saveInPackageLayout.RectTransform), + onUpdate: (f, component) => + { + bool canCreateNewPackage = true; + foreach (GUIComponent contentChild in packageToSaveInList.Content.Children) + { + contentChild.Visible = !(contentChild.UserData is ContentPackage p) + || !string.Equals(p.Name, nameBox.Text, StringComparison.OrdinalIgnoreCase); + canCreateNewPackage &= contentChild.Visible; + contentChild.Visible &= !(contentChild.GetChild()?.GetChild() is GUITextBlock tb && + !tb.Text.Contains(packToSaveInFilter.Text, StringComparison.OrdinalIgnoreCase)); + } + + if (newPackageListIcon.Style.Identifier != "NewContentPackageIcon" && canCreateNewPackage) + { + GUIStyle.Apply(newPackageListIcon, "NewContentPackageIcon"); + newPackageListText.Text = TextManager.Get("CreateNewLocalPackage"); + } + if (newPackageListIcon.Style.Identifier != "WorkshopMenu.EditButton" && !canCreateNewPackage) + { + GUIStyle.Apply(newPackageListIcon, "WorkshopMenu.EditButton"); + newPackageListText.Text = TextManager.GetWithVariable("UpdateExistingLocalPackage", "[mod]", nameBox.Text); + } + }); + packageToSaveInList.Select(0); + ContentPackage ownerPkg = null; + if (MainSub?.Info != null) { ownerPkg = GetLocalPackageThatOwnsSub(MainSub.Info); } + foreach (var p in ContentPackageManager.LocalPackages) + { + addItemToPackageToSaveList(p.Name, p); + } + + if (ownerPkg != null && !string.Equals(ownerPkg.Name, nameBox.Text, StringComparison.OrdinalIgnoreCase)) + { + packageToSaveInList.Select(ownerPkg); + packageToSaveInList.ScrollToElement(packageToSaveInList.SelectedComponent); + } + + var requiredContentPackagesLayout = new GUILayoutGroup(new RectTransform(Vector2.One, + horizontalArea.RectTransform, Anchor.BottomRight)) + { + Stretch = true, + Visible = false + }; + + var requiredContentPackList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), + requiredContentPackagesLayout.RectTransform)); + + var filterLayout = new GUILayoutGroup( + new RectTransform((1.0f, 0.15f), requiredContentPackagesLayout.RectTransform), + isHorizontal: true, childAnchor: Anchor.CenterLeft); + var contentPackFilter - = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.0f), contentPackagesLayout.RectTransform), + = new GUITextBox(new RectTransform((0.6f, 1.0f), filterLayout.RectTransform), createClearButton: true); contentPackFilter.OnTextChanged += (box, text) => { - contentPackList.Content.Children.ForEach(c + requiredContentPackList.Content.Children.ForEach(c => c.Visible = !(c is GUITickBox tb && !tb.Text.Contains(text, StringComparison.OrdinalIgnoreCase))); return true; }; + var autoDetectBtn = new GUIButton(new RectTransform((0.4f, 1.0f), filterLayout.RectTransform), + text: TextManager.Get("AutoDetectRequiredPackages"), style: "GUIButtonSmall") + { + OnClicked = (button, o) => + { + var requiredPackages = MapEntity.mapEntityList.Select(e => e.Prefab.ContentPackage) + .Distinct().OfType().Select(p => p.Name).ToHashSet(); + var tickboxes = requiredContentPackList.Content.Children.OfType().ToArray(); + tickboxes.ForEach(tb => tb.Selected = requiredPackages.Contains(tb.UserData as string ?? "")); + return false; + } + }; + if (MainSub != null) { List allContentPacks = MainSub.Info.RequiredContentPackages.ToList(); @@ -2596,7 +2776,7 @@ namespace Barotrauma foreach (string contentPackageName in allContentPacks) { - var cpTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), contentPackList.Content.RectTransform), contentPackageName, font: GUIStyle.SmallFont) + var cpTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.2f), requiredContentPackList.Content.RectTransform), contentPackageName, font: GUIStyle.SmallFont) { Selected = MainSub.Info.RequiredContentPackages.Contains(contentPackageName), UserData = contentPackageName @@ -2616,7 +2796,19 @@ namespace Barotrauma } } + GUIButton.OnClickedHandler switchToTab(GUIButton tabBtn, GUIComponent tab) + => (button, obj) => + { + horizontalArea.Children.ForEach(c => c.Visible = false); + contentPackageTabber.Children.ForEach(c => c.Selected = false); + tabBtn.Selected = true; + tab.Visible = true; + return false; + }; + saveToPackageTabBtn.OnClicked = switchToTab(saveToPackageTabBtn, saveInPackageLayout); + reqPackagesTabBtn.OnClicked = switchToTab(reqPackagesTabBtn, requiredContentPackagesLayout); + var buttonArea = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), paddedSaveFrame.RectTransform, Anchor.BottomCenter, minSize: new Point(0, 30)), style: null); var cancelButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonArea.RectTransform, Anchor.BottomLeft), @@ -2632,22 +2824,23 @@ namespace Barotrauma var saveButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonArea.RectTransform, Anchor.BottomRight), TextManager.Get("SaveSubButton")) { - OnClicked = SaveSub + OnClicked = (button, o) => SaveSub(packageToSaveInList.SelectedData as ContentPackage) }; paddedSaveFrame.Recalculate(); leftColumn.Recalculate(); - subSettingsContainer.RectTransform.MinSize = outpostSettingsContainer.RectTransform.MinSize = + subSettingsContainer.RectTransform.MinSize = outpostSettingsContainer.RectTransform.MinSize = beaconSettingsContainer.RectTransform.MinSize = new Point(0, Math.Max(subSettingsContainer.Rect.Height, outpostSettingsContainer.Rect.Height)); subSettingsContainer.Recalculate(); outpostSettingsContainer.Recalculate(); + beaconSettingsContainer.Recalculate(); descriptionBox.Text = MainSub == null ? "" : MainSub.Info.Description.Value; submarineDescriptionCharacterCount.Text = descriptionBox.Text.Length + " / " + submarineDescriptionLimit; subTypeDropdown.SelectItem(MainSub.Info.Type); - if (quickSave) { SaveSub(saveButton, saveButton.UserData); } + if (quickSave) { SaveSub(null); } } private void CreateSaveAssemblyScreen() @@ -2774,16 +2967,7 @@ namespace Barotrauma } else { - var identifier = nameBox.Text.ToLowerInvariant().Replace(" ", ""); - var existingPrefab = MapEntityPrefab.Find(null, identifier, showErrorMessages: false); - if (existingPrefab != null && System.IO.Path.GetDirectoryName(existingPrefab.FilePath.Value) == ItemAssemblyPrefab.VanillaSaveFolder) - { - var msgBox = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("ItemAssemblyVanillaFileExistsWarning")); - } - else - { - Save(); - } + Save(); } void Save() @@ -2792,7 +2976,7 @@ namespace Barotrauma if (existingContentPackage == null) { //content package doesn't exist, create one - ModProject modProject = new ModProject() { Name = nameBox.Text }; + ModProject modProject = new ModProject { Name = nameBox.Text }; var newFile = ModProject.File.FromPath(Path.Combine(ContentPath.ModDirStr, $"{nameBox.Text}.xml")); modProject.AddFile(newFile); string newPackagePath = ContentPackageManager.LocalPackages.SaveRegularMod(modProject); @@ -2853,6 +3037,13 @@ namespace Barotrauma } } + private IEnumerable GetLoadableSubs() + { + string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); + return SubmarineInfo.SavedSubmarines.Where(s + => Path.GetDirectoryName(Path.GetFullPath(s.FilePath)) != downloadFolder); + } + private void CreateLoadScreen() { CloseItem(); @@ -2887,6 +3078,7 @@ namespace Barotrauma var subList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.7f), paddedLoadFrame.RectTransform)) { + PlaySoundOnSelect = true, ScrollBarVisible = true, OnSelected = (GUIComponent selected, object userData) => { @@ -2899,7 +3091,7 @@ namespace Barotrauma return true; } - var package = GetContentPackageIntrinsicallyTiedToSub(subInfo); + var package = GetLocalPackageThatOwnsSub(subInfo); if (package != null) { deleteBtn.Enabled = true; @@ -2907,11 +3099,11 @@ namespace Barotrauma else { deleteBtn.Enabled = false; - if (ContentPackageManager.VanillaCorePackage?.Files.Any(f => f.Path == subInfo.FilePath) ?? false) + if (IsVanillaSub(subInfo)) { deleteBtn.ToolTip = TextManager.Get("cantdeletevanillasub"); } - else if (ContentPackageManager.AllPackages.FirstOrDefault(p => p.Files.Any(f => f.Path == subInfo.FilePath)) is ContentPackage subPackage) + else if (GetPackageThatOwnsSub(subInfo, ContentPackageManager.AllPackages) is ContentPackage subPackage) { deleteBtn.ToolTip = TextManager.GetWithVariable("cantdeletemodsub", "[modname]", subPackage.Name); } @@ -2925,9 +3117,10 @@ namespace Barotrauma searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; }; - string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); - List sortedSubs = new List(SubmarineInfo.SavedSubmarines.Where(s => Path.GetDirectoryName(Path.GetFullPath(s.FilePath)) != downloadFolder)); - sortedSubs.Sort((s1, s2) => { return s1.Type.CompareTo(s2.Type) * 100 + s1.Name.CompareTo(s2.Name); }); + var sortedSubs = GetLoadableSubs() + .OrderBy(s => s.Type) + .ThenBy(s => s.Name) + .ToList(); SubmarineInfo prevSub = null; @@ -2968,7 +3161,7 @@ namespace Barotrauma if (!(ContentPackageManager.VanillaCorePackage?.Files.Any(f => f.Path == sub.FilePath) ?? false)) { - if (GetContentPackageIntrinsicallyTiedToSub(sub) == null && + if (GetLocalPackageThatOwnsSub(sub) == null && ContentPackageManager.AllPackages.FirstOrDefault(p => p.Files.Any(f => f.Path == sub.FilePath)) is ContentPackage subPackage) { //workshop mod @@ -3080,7 +3273,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), controlBtnHolder.RectTransform, Anchor.BottomRight), TextManager.Get("Load")) { - OnClicked = LoadSub + OnClicked = HitLoadSubButton }; controlBtnHolder.RectTransform.MaxSize = new Point(int.MaxValue, controlBtnHolder.Children.First().Rect.Height); @@ -3117,9 +3310,9 @@ namespace Barotrauma /// Recovers the auto saved submarine /// /// - private void LoadAutoSave(object UserData) + private void LoadAutoSave(object userData) { - if (!(UserData is XElement element)) { return; } + if (!(userData is XElement element)) { return; } #warning TODO: revise string filePath = element.GetAttributeStringUnrestricted("file", ""); @@ -3152,7 +3345,7 @@ namespace Barotrauma loadFrame = null; } - private bool LoadSub(GUIButton button, object obj) + private bool HitLoadSubButton(GUIButton button, object obj) { if (loadFrame == null) { @@ -3167,14 +3360,68 @@ namespace Barotrauma return false; } - if (subList.SelectedComponent == null) { return false; } - if (!(subList.SelectedComponent.UserData is SubmarineInfo selectedSubInfo)) { return false; } + if (!(subList.SelectedComponent?.UserData is SubmarineInfo selectedSubInfo)) { return false; } - LoadSub(selectedSubInfo); - - return true; + var ownerPackage = GetLocalPackageThatOwnsSub(selectedSubInfo); + if (ownerPackage is null) + { + if (GetWorkshopPackageThatOwnsSub(selectedSubInfo) is ContentPackage workshopPackage) + { + if (publishedWorkshopItemIds.Contains(workshopPackage.SteamWorkshopId)) + { + AskLoadPublishedSub(selectedSubInfo, workshopPackage); + } + else + { + AskLoadSubscribedSub(selectedSubInfo); + } + } + else if (IsVanillaSub(selectedSubInfo)) + { +#if DEBUG + LoadSub(selectedSubInfo); +#else + AskLoadVanillaSub(selectedSubInfo); +#endif + } + } + else + { + LoadSub(selectedSubInfo); + } + return false; } + void AskLoadSub(SubmarineInfo info, LocalizedString header, LocalizedString desc) + { + var msgBox = new GUIMessageBox( + header, + desc, + new[] { TextManager.Get("LoadAnyway"), TextManager.Get("Cancel") }); + msgBox.Buttons[0].OnClicked = (button, o) => + { + LoadSub(info); + msgBox.Close(); + return false; + }; + msgBox.Buttons[1].OnClicked = msgBox.Close; + } + + void AskLoadPublishedSub(SubmarineInfo info, ContentPackage pkg) + => AskLoadSub(info, + TextManager.Get("LoadingPublishedSubmarineHeader"), + TextManager.GetWithVariable("LoadingPublishedSubmarineDesc", "[modname]", pkg.Name)); + + void AskLoadSubscribedSub(SubmarineInfo info) + => AskLoadSub(info, + TextManager.Get("LoadingSubscribedSubmarineHeader"), + TextManager.Get("LoadingSubscribedSubmarineDesc")); + + void AskLoadVanillaSub(SubmarineInfo info) + => AskLoadSub(info, + TextManager.Get("LoadingVanillaSubmarineHeader"), + TextManager.Get("LoadingVanillaSubmarineDesc")); + public void LoadSub(SubmarineInfo info) { Submarine.Unload(); @@ -3202,7 +3449,10 @@ namespace Barotrauma { if (item.ParentInventory != null || item.body != null) continue; var lightComponent = item.GetComponent(); - if (lightComponent != null) lightComponent.LightColor = new Color(lightComponent.LightColor, lightComponent.LightColor.A / 255.0f * 0.5f); + foreach (var light in item.GetComponents()) + { + light.LightColor = new Color(light.LightColor, light.LightColor.A / 255.0f * 0.5f); + } } new GUIMessageBox("", TextManager.Get("AdjustedLightsNotification")); return true; @@ -3213,11 +3463,18 @@ namespace Barotrauma ReconstructLayers(); } - private static RegularPackage GetContentPackageIntrinsicallyTiedToSub(SubmarineInfo sub) - => ContentPackageManager.LocalPackages.Regular - .Where(p => p.Files.Length == 1) - .FirstOrDefault(regularPackage => regularPackage.Files[0].Path == sub.FilePath); + private static ContentPackage GetPackageThatOwnsSub(SubmarineInfo sub, IEnumerable packages) + => packages.FirstOrDefault(package => package.Files.Any(f => f.Path == sub.FilePath)); + private static ContentPackage GetLocalPackageThatOwnsSub(SubmarineInfo sub) + => GetPackageThatOwnsSub(sub, ContentPackageManager.LocalPackages); + + private static ContentPackage GetWorkshopPackageThatOwnsSub(SubmarineInfo sub) + => GetPackageThatOwnsSub(sub, ContentPackageManager.WorkshopPackages); + + private static bool IsVanillaSub(SubmarineInfo sub) + => GetPackageThatOwnsSub(sub, ContentPackageManager.VanillaCorePackage.ToEnumerable()) != null; + private void TryDeleteSub(SubmarineInfo sub) { if (sub == null) { return; } @@ -3225,7 +3482,7 @@ namespace Barotrauma //If the sub is included in a content package that only defines that one sub, //check that it's a local content package and only allow deletion if it is. //(deleting from the Submarines folder is also currently allowed, but this is temporary) - var subPackage = GetContentPackageIntrinsicallyTiedToSub(sub); + var subPackage = GetLocalPackageThatOwnsSub(sub); if (!ContentPackageManager.LocalPackages.Regular.Contains(subPackage)) { return; } var msgBox = new GUIMessageBox( @@ -3238,9 +3495,11 @@ namespace Barotrauma { if (subPackage != null) { - Directory.Delete(Path.GetDirectoryName(subPackage.Path), recursive: true); - ContentPackageManager.LocalPackages.Refresh(); - ContentPackageManager.EnabledPackages.DisableRemovedMods(); + File.Delete(sub.FilePath); + ModProject modProject = new ModProject(subPackage); + modProject.RemoveFile(modProject.Files.First(f => ContentPath.FromRaw(subPackage, f.Path) == sub.FilePath)); + modProject.Save(subPackage.Path); + ReloadModifiedPackage(subPackage); } sub.Dispose(); @@ -3869,6 +4128,7 @@ namespace Barotrauma GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center)) { + PlaySoundOnSelect = true, OnSelected = SelectWire }; @@ -4846,7 +5106,7 @@ namespace Barotrauma int index = key == Keys.D0 ? numberKeys.Count : numberKeys.IndexOf(key) - 1; if (index > -1 && index < listBox.Content.CountChildren) { - listBox.Select(index, force: false, autoScroll: true, takeKeyBoardFocus: false); + listBox.Select(index); SkipInventorySlotUpdate = true; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index fb767cd11..afd88b6da 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -1282,6 +1282,7 @@ namespace Barotrauma var textList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), msgBox.Content.RectTransform, Anchor.TopCenter)) { + PlaySoundOnSelect = true, OnSelected = (component, userData) => { string text = userData as string ?? ""; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index d4ef4f065..45c0e1c6a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -171,7 +171,7 @@ namespace Barotrauma int childIndex = values.IndexOf(currentValue); dropdown.Select(childIndex); dropdown.ListBox.ForceLayoutRecalculation(); - dropdown.ListBox.ScrollToElement(dropdown.ListBox.Content.GetChild(childIndex), playSound: false); + dropdown.ListBox.ScrollToElement(dropdown.ListBox.Content.GetChild(childIndex)); dropdown.OnSelected = (dd, obj) => { setter((T)obj); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 821725532..0705c3084 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -418,7 +418,7 @@ namespace Barotrauma } else { - if (!Level.IsLoadedOutpost && Character.Controlled?.CurrentHull?.Submarine is Submarine sub && + if (!Level.IsLoadedFriendlyOutpost && Character.Controlled?.CurrentHull?.Submarine is Submarine sub && sub.Info != null && !sub.Info.IsOutpost) { hullSoundSource = Character.Controlled.CurrentHull; @@ -889,5 +889,13 @@ namespace Barotrauma .Where(s => s.Type == soundType) .GetRandomUnsynced()?.Sound?.Play(null, "ui"); } + + public static void PlayUISound(GUISoundType? soundType) + { + if (soundType.HasValue) + { + PlayUISound(soundType.Value); + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index 7696fbefe..a21ef24c9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -176,12 +176,15 @@ namespace Barotrauma.Steam Directory.CreateDirectory(PublishStagingDir); await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, PublishStagingDir, ShouldCorrectPaths.No); + var stagingFileListPath = Path.Combine(PublishStagingDir, ContentPackage.FileListFileName); + ContentPackage tempPkg = ContentPackage.TryLoad(stagingFileListPath) ?? throw new Exception("Staging copy could not be loaded"); + //Load filelist.xml and write the hash into it so anyone downloading this mod knows what it should be - ModProject modProject = new ModProject(contentPackage) + ModProject modProject = new ModProject(tempPkg) { ModVersion = modVersion }; - modProject.Save(Path.Combine(PublishStagingDir, ContentPackage.FileListFileName)); + modProject.Save(stagingFileListPath); } public static async Task CreateLocalCopy(ContentPackage contentPackage) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs index 484fe3182..7cc29acce 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs @@ -46,7 +46,10 @@ namespace Barotrauma.Steam regularBox.CanBeFocused = true; } } - filterBox = CreateSearchBox(mainLayout, width: 1.0f); + + var searchRectT = NewItemRectT(mainLayout, heightScale: 1.0f); + searchRectT.RelativeSize = (1.0f, searchRectT.RelativeSize.Y); + filterBox = CreateSearchBox(searchRectT); Label(mainLayout, TextManager.Get("CannotChangeMods"), GUIStyle.Font); } @@ -55,9 +58,8 @@ namespace Barotrauma.Steam { string str = filterBox.Text; regularList.Content.Children - .ForEach(c => c.Visible = str.IsNullOrWhiteSpace() - || (c.UserData is ContentPackage p - && p.Name.Contains(str, StringComparison.OrdinalIgnoreCase))); + .ForEach(c => c.Visible = !(c.UserData is ContentPackage p) + || ModNameMatches(p, str)); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs new file mode 100644 index 000000000..59624f305 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs @@ -0,0 +1,746 @@ +#nullable enable +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using ItemOrPackage = Barotrauma.Either; + +namespace Barotrauma.Steam +{ + sealed partial class MutableWorkshopMenu : WorkshopMenu + { + private CorePackage EnabledCorePackage => enabledCoreDropdown.SelectedData as CorePackage ?? throw new Exception("Valid core package not selected"); + + private readonly GUIDropDown enabledCoreDropdown; + private readonly GUIListBox enabledRegularModsList; + private readonly GUIListBox disabledRegularModsList; + private readonly Action onInstalledInfoButtonHit; + private readonly GUITextBox modsListFilter; + private readonly Dictionary modsListFilterTickboxes; + private readonly GUIButton bulkUpdateButton; + + private GUIComponent? draggedElement = null; + private GUIListBox? draggedElementOrigin = null; + + private void UpdateSubscribedModInstalls() + { + if (!SteamManager.IsInitialized) { return; } + + uint numSubscribedMods = SteamManager.GetNumSubscribedItems(); + if (numSubscribedMods == memSubscribedModCount) { return; } + memSubscribedModCount = numSubscribedMods; + + var subscribedIds = SteamManager.GetSubscribedItems().ToHashSet(); + var installedIds = ContentPackageManager.WorkshopPackages.Select(p => p.SteamWorkshopId).ToHashSet(); + foreach (var id in subscribedIds.Where(id2 => !installedIds.Contains(id2))) + { + Steamworks.Ugc.Item item = new Steamworks.Ugc.Item(id); + if (!item.IsDownloading && !SteamManager.Workshop.IsInstalling(item)) + { + SteamManager.Workshop.DownloadModThenEnqueueInstall(item); + } + } + + TaskPool.Add("RemoveUnsubscribedItems", SteamManager.Workshop.GetPublishedItems(), t => + { + if (!t.TryGetResult(out ISet publishedItems)) { return; } + + var allRequiredInstalled = subscribedIds.Union(publishedItems.Select(it => it.Id)).ToHashSet(); + bool needsRefresh = false; + foreach (var id in installedIds.Where(id2 => !allRequiredInstalled.Contains(id2))) + { + Steamworks.Ugc.Item item = new Steamworks.Ugc.Item(id); + SteamManager.Workshop.Uninstall(item); + needsRefresh = true; + } + + if (needsRefresh) + { + PopulateInstalledModLists(); + } + }); + } + + private static (GUILayoutGroup Left, GUIFrame center, GUILayoutGroup Right) CreateSidebars( + GUIComponent parent, + float leftWidth = 0.3875f, + float centerWidth = 0.025f, + float rightWidth = 0.5875f, + bool split = false, + float height = 1.0f) + { + GUILayoutGroup layout = new GUILayoutGroup(new RectTransform((1.0f, height), parent.RectTransform), isHorizontal: true); + GUILayoutGroup left = new GUILayoutGroup(new RectTransform((leftWidth, 1.0f), layout.RectTransform), isHorizontal: false); + var center = new GUIFrame(new RectTransform((centerWidth, 1.0f), layout.RectTransform), style: null); + if (split) + { + new GUICustomComponent(new RectTransform(Vector2.One, center.RectTransform), + onDraw: (sb, c) => + { + sb.DrawLine((c.Rect.Center.X, c.Rect.Top), (c.Rect.Center.X, c.Rect.Bottom), GUIStyle.TextColorDim, 2f); + }); + } + GUILayoutGroup right = new GUILayoutGroup(new RectTransform((rightWidth, 1.0f), layout.RectTransform), isHorizontal: false); + return (left, center, right); + } + + private void HandleDraggingAcrossModLists(GUIListBox from, GUIListBox to) + { + if (to.Rect.Contains(PlayerInput.MousePosition) && from.DraggedElement != null) + { + //move the dragged elements to the index determined previously + var draggedElement = from.DraggedElement; + + var selected = from.AllSelected.ToList(); + selected.Sort((a, b) => from.Content.GetChildIndex(a) - from.Content.GetChildIndex(b)); + + float oldCount = to.Content.CountChildren; + float newCount = oldCount + selected.Count; + + var offset = draggedElement.RectTransform.AbsoluteOffset; + offset += from.Content.Rect.Location; + offset -= to.Content.Rect.Location; + + for (int i = 0; i < selected.Count; i++) + { + var c = selected[i]; + c.Parent.RemoveChild(c); + c.RectTransform.Parent = to.Content.RectTransform; + c.RectTransform.RepositionChildInHierarchy((int)oldCount+i); + } + + from.DraggedElement = null; + from.Deselect(); + from.RecalculateChildren(); + from.RectTransform.RecalculateScale(true); + to.RecalculateChildren(); + to.RectTransform.RecalculateScale(true); + to.Select(selected); + + //recalculate the dragged element's offset so it doesn't jump around + draggedElement.RectTransform.AbsoluteOffset = offset; + + to.DraggedElement = draggedElement; + + to.BarScroll *= (oldCount / newCount); + } + } + + private Action? currentSwapFunc = null; + private GUISoundType? swapSoundType = null; + + private void PlaySwapSound() + { + SoundPlayer.PlayUISound(swapSoundType); + } + + private void SetSwapFunc(GUIListBox from, GUIListBox to) + { + currentSwapFunc = () => + { + to.Deselect(); + var selected = from.AllSelected.ToArray(); + foreach (var frame in selected) + { + frame.Parent.RemoveChild(frame); + frame.RectTransform.Parent = to.Content.RectTransform; + } + from.RecalculateChildren(); + from.RectTransform.RecalculateScale(true); + to.RecalculateChildren(); + to.RectTransform.RecalculateScale(true); + to.Select(selected); + }; + + if (to == enabledRegularModsList) + { + swapSoundType = GUISoundType.Increase; + } + else if (to == disabledRegularModsList) + { + swapSoundType = GUISoundType.Decrease; + } + else + { + swapSoundType = null; + } + } + + private void CreateInstalledModsTab( + out GUIDropDown enabledCoreDropdown, + out GUIListBox enabledRegularModsList, + out GUIListBox disabledRegularModsList, + out Action onInstalledInfoButtonHit, + out GUITextBox modsListFilter, + out Dictionary modsListFilterTickboxes, + out GUIButton bulkUpdateButton) + { + GUIFrame content = CreateNewContentFrame(Tab.InstalledMods); + + CreateWorkshopItemDetailContainer( + content, + out var outerContainer, + onSelected: (itemOrPackage, selectedFrame) => + { + if (itemOrPackage.TryGet(out Steamworks.Ugc.Item item)) { PopulateFrameWithItemInfo(item, selectedFrame); } + }, + onDeselected: () => PopulateInstalledModLists(), + out onInstalledInfoButtonHit, out var deselect); + + GUILayoutGroup mainLayout = + new GUILayoutGroup(new RectTransform(Vector2.One, outerContainer.Content.RectTransform), childAnchor: Anchor.TopCenter); + mainLayout.RectTransform.SetAsFirstChild(); + + var (topLeft, _, topRight) = CreateSidebars(mainLayout, centerWidth: 0.05f, leftWidth: 0.475f, rightWidth: 0.475f, height: 0.13f); + topLeft.Stretch = true; + Label(topLeft, TextManager.Get("enabledcore"), GUIStyle.SubHeadingFont, heightScale: 1.0f); + enabledCoreDropdown = Dropdown(topLeft, + (p) => p.Name, + ContentPackageManager.CorePackages.ToArray(), + ContentPackageManager.EnabledPackages.Core!, + (p) => { }, + heightScale: 1.0f / 13.0f); + Label(topLeft, "", GUIStyle.SubHeadingFont, heightScale: 1.0f); + topRight.ChildAnchor = Anchor.CenterLeft; + + var topRightButtons = new GUILayoutGroup(new RectTransform((1.0f, 0.5f), topRight.RectTransform), + isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + + void padTopRight(float width=1.0f) + { + new GUIFrame(new RectTransform((width, 1.0f), topRightButtons.RectTransform), style: null); + } + + padTopRight(); + //TODO: put stuff here + padTopRight(width: 3.0f); + var refreshListsButton + = new GUIButton( + new RectTransform(Vector2.One, topRightButtons.RectTransform, scaleBasis: ScaleBasis.BothHeight), + text: "", style: "GUIReloadButton") + { + OnClicked = (b, o) => + { + PopulateInstalledModLists(); + return false; + }, + ToolTip = TextManager.Get("RefreshModLists") + }; + bulkUpdateButton + = new GUIButton( + new RectTransform(Vector2.One, topRightButtons.RectTransform, scaleBasis: ScaleBasis.BothHeight), + text: "", style: "GUIUpdateButton") + { + OnClicked = (b, o) => + { + BulkDownloader.PrepareUpdates(); + return false; + }, + Enabled = false + }; + padTopRight(width: 0.1f); + + var (left, center, right) = CreateSidebars(mainLayout, centerWidth: 0.05f, leftWidth: 0.475f, rightWidth: 0.475f, height: 0.8f); + right.ChildAnchor = Anchor.TopRight; + + //enabled mods + Label(left, TextManager.Get("enabledregular"), GUIStyle.SubHeadingFont); + var enabledModsList = new GUIListBox(new RectTransform((1.0f, 0.93f), left.RectTransform)) + { + CurrentDragMode = GUIListBox.DragMode.DragOutsideBox, + CurrentSelectMode = GUIListBox.SelectMode.RequireShiftToSelectMultiple, + HideDraggedElement = true, + PlaySoundOnSelect = true, + SoundOnDragStart = GUISoundType.Select, + SoundOnDragStop = GUISoundType.Increase, + }; + enabledRegularModsList = enabledModsList; + + //disabled mods + Label(right, TextManager.Get("disabledregular"), GUIStyle.SubHeadingFont); + var disabledModsList = new GUIListBox(new RectTransform((1.0f, 0.93f), right.RectTransform)) + { + CurrentDragMode = GUIListBox.DragMode.DragOutsideBox, + CurrentSelectMode = GUIListBox.SelectMode.RequireShiftToSelectMultiple, + HideDraggedElement = true, + PlaySoundOnSelect = true, + SoundOnDragStart = GUISoundType.Select, + SoundOnDragStop = GUISoundType.Decrease, + }; + disabledRegularModsList = disabledModsList; + + var centerButton = + new GUIButton( + new RectTransform(Vector2.One * 0.95f, center.RectTransform, scaleBasis: ScaleBasis.BothWidth, + anchor: Anchor.Center), + style: "GUIButtonToggleLeft") + { + PlaySoundOnSelect = false, + Visible = false, + OnClicked = (button, o) => + { + if (currentSwapFunc != null) + { + PlaySwapSound(); + currentSwapFunc.Invoke(); + } + return false; + } + }; + + enabledModsList.OnSelected = (frame, o) => + { + disabledModsList.Deselect(); + + centerButton.Visible = true; + centerButton.ApplyStyle(GUIStyle.GetComponentStyle("GUIButtonToggleRight")); + + SetSwapFunc(enabledModsList, disabledModsList); + + return true; + }; + disabledModsList.OnSelected = (frame, o) => + { + enabledModsList.Deselect(); + + centerButton.Visible = true; + centerButton.ApplyStyle(GUIStyle.GetComponentStyle("GUIButtonToggleLeft")); + + SetSwapFunc(disabledModsList, enabledModsList); + + return true; + }; + + var filterContainer = new GUILayoutGroup(NewItemRectT(mainLayout, heightScale: 1.0f), isHorizontal: true) + { Stretch = true, RelativeSpacing = 0.01f }; + + void padFilterContainer(float width = 0.25f) + => new GUIFrame(new RectTransform((width, 1.0f), filterContainer!.RectTransform), style: null); + + GUIButton filterLayoutButton(string style) + => new GUIButton( + new RectTransform(Vector2.One, filterContainer!.RectTransform, scaleBasis: ScaleBasis.BothHeight), + "", style: style); + + padFilterContainer(width: 0.2f); + var loadPresetBtn = filterLayoutButton("OpenButton"); + loadPresetBtn.ToolTip = TextManager.Get("LoadModListPresetHeader"); + loadPresetBtn.OnClicked = OpenLoadPreset; + var savePresetBtn = filterLayoutButton("SaveButton"); + savePresetBtn.ToolTip = TextManager.Get("SaveModListPresetHeader"); + savePresetBtn.OnClicked = OpenSavePreset; + padFilterContainer(width: 0.05f); + var searchRectT = new RectTransform((0.5f, 1.0f), filterContainer.RectTransform); + var searchBox = CreateSearchBox(searchRectT); + modsListFilter = searchBox; + + var filterTickboxes = new Dictionary(); + modsListFilterTickboxes = filterTickboxes; + + var filterTickboxesDropdown + = filterLayoutButton("SetupVisibilityButton"); + var filterTickboxesContainer + = new GUIFrame(new RectTransform((0.3f, 0.2f), content.RectTransform, + scaleBasis: ScaleBasis.BothWidth), style: "InnerFrame"); + var filterTickboxesUpdater + = new GUICustomComponent(new RectTransform(Vector2.Zero, content.RectTransform), + onUpdate: (f, component) => + { + filterTickboxesContainer.Visible = filterTickboxesDropdown.Selected; + filterTickboxesContainer.RectTransform.AbsoluteOffset + = (filterTickboxesDropdown.Rect.Location - content.Rect.Location) + + (filterTickboxesDropdown.Rect.Width / 2, 0) + - (filterTickboxesContainer.Rect.Size.ToVector2() * (0.5f, 1.0f)).ToPoint(); + filterTickboxesContainer.RectTransform.NonScaledSize + = new Point(filterTickboxes.Select(tb => (int)tb.Value.Font.MeasureString(tb.Value.GetChild().Text).X).Max(), + filterTickboxes.Select(tb => tb.Value.Rect.Height).Aggregate((a,b) => a+b)) + +(filterTickboxes.Values.First().Rect.Height * 4, filterTickboxes.Values.First().Rect.Height / 2); + if (PlayerInput.PrimaryMouseButtonClicked() + && !GUI.IsMouseOn(filterTickboxesDropdown) + && !GUI.IsMouseOn(filterTickboxesContainer)) + { + filterTickboxesDropdown.Selected = false; + } + }); + + var filterTickboxesLayout + = new GUILayoutGroup(new RectTransform(Vector2.One * 0.95f, filterTickboxesContainer.RectTransform, Anchor.Center)); + + void addFilterTickbox(Filter filter, string? style, bool selected) + { + var tickbox = new GUITickBox(NewItemRectT(filterTickboxesLayout!, heightScale: 0.5f), "") + { + Selected = selected, + OnSelected = _ => + { + UpdateModListItemVisibility(); + return true; + } + }; + filterTickboxes!.Add(filter, tickbox); + var text = new GUITextBlock(new RectTransform((1.0f, 1.0f), tickbox.RectTransform, Anchor.CenterRight) + { + AbsoluteOffset = (-tickbox.Box.Rect.Width * 2, 0), + }, + TextManager.Get($"ModFilter.{filter}")) + { + CanBeFocused = false + }; + var icon = new GUIFrame( + new RectTransform(Vector2.One, text.RectTransform, Anchor.CenterLeft, Pivot.CenterRight, + scaleBasis: ScaleBasis.BothHeight), style: style) + { + CanBeFocused = false + }; + } + + addFilterTickbox(Filter.ShowLocal, "WorkshopMenu.EditButton", selected: true); + addFilterTickbox(Filter.ShowWorkshop, "WorkshopMenu.DownloadedIcon", selected: true); + addFilterTickbox(Filter.ShowPublished, "WorkshopMenu.PublishedIcon", selected: true); + addFilterTickbox(Filter.ShowOnlySubs, null, selected: false); + addFilterTickbox(Filter.ShowOnlyItemAssemblies, null, selected: false); + + padFilterContainer(); + + new GUICustomComponent(new RectTransform(Vector2.Zero, content.RectTransform), + onUpdate: (f, component) => + { + HandleDraggingAcrossModLists(enabledModsList, disabledModsList); + HandleDraggingAcrossModLists(disabledModsList, enabledModsList); + UpdateDraggingSounds(); + + if (PlayerInput.PrimaryMouseButtonClicked() + && !GUI.IsMouseOn(enabledModsList) + && !GUI.IsMouseOn(disabledModsList) + && GUIContextMenu.CurrentContextMenu is null) + { + enabledModsList.Deselect(); + disabledModsList.Deselect(); + } + else if (!PlayerInput.IsCtrlDown() && !PlayerInput.IsShiftDown() && PlayerInput.DoubleClicked()) + { + currentSwapFunc?.Invoke(); + } + }, + onDraw: (spriteBatch, component) => + { + enabledModsList.DraggedElement?.DrawManually(spriteBatch, true, true); + disabledModsList.DraggedElement?.DrawManually(spriteBatch, true, true); + }); + + void UpdateDraggingSounds() + { + if (draggedElement != null) + { + if (enabledModsList.DraggedElement == null && disabledModsList.DraggedElement == null) + { + SetDragOrigin(null); + } + CheckDragStopSound(enabledModsList); + CheckDragStopSound(disabledModsList); + } + else if (enabledModsList.DraggedElement != null) + { + SetDragOrigin(enabledModsList); + } + else if (disabledModsList.DraggedElement != null) + { + SetDragOrigin(disabledModsList); + } + + void SetDragOrigin(GUIListBox? listBox) + { + draggedElement = listBox?.DraggedElement; + draggedElementOrigin = listBox; + } + + void CheckDragStopSound(GUIListBox listBox) + { + listBox.PlaySoundOnDragStop = listBox.DraggedElement != null && draggedElementOrigin != listBox; + } + } + } + + protected override void UpdateModListItemVisibility() + { + string str = modsListFilter.Text; + enabledRegularModsList.Content.Children.Concat(disabledRegularModsList.Content.Children) + .ForEach(c => c.Visible = !(c.UserData is ContentPackage p) + || ModNameMatches(p, str) && ModMatchesTickboxes(p, c)); + } + + private bool ModMatchesTickboxes(ContentPackage p, GUIComponent guiItem) + { + var iconBtn = guiItem.GetChild()?.GetAllChildren().Last(); + + bool matches = false; + matches |= modsListFilterTickboxes[Filter.ShowLocal].Selected + && ContentPackageManager.LocalPackages.Contains(p); + matches |= modsListFilterTickboxes[Filter.ShowPublished].Selected + && (ContentPackageManager.WorkshopPackages.Contains(p) + && iconBtn?.Style?.Identifier == "WorkshopMenu.PublishedIcon"); + matches |= modsListFilterTickboxes[Filter.ShowWorkshop].Selected + && (ContentPackageManager.WorkshopPackages.Contains(p) + && iconBtn?.Style?.Identifier != "WorkshopMenu.PublishedIcon"); + + if (modsListFilterTickboxes[Filter.ShowOnlySubs].Selected + && modsListFilterTickboxes[Filter.ShowOnlyItemAssemblies].Selected + && p.Files.All(f => f is BaseSubFile || f is ItemAssemblyFile)) + { + //Both the subs-only tickbox and the item-assembly-only tickbox + //are enabled, and all files match either of them so show this mod + } + else if (modsListFilterTickboxes[Filter.ShowOnlySubs].Selected + && p.Files.Any(f => !(f is BaseSubFile))) + { + matches = false; + } + else if (modsListFilterTickboxes[Filter.ShowOnlyItemAssemblies].Selected + && p.Files.Any(f => !(f is ItemAssemblyFile))) + { + matches = false; + } + + return matches; + } + + private void PrepareToShowModInfo(ContentPackage mod) + { + TaskPool.Add($"PrepareToShow{mod.SteamWorkshopId}Info", SteamManager.Workshop.GetItem(mod.SteamWorkshopId), + t => + { + if (!t.TryGetResult(out Steamworks.Ugc.Item? item)) { return; } + if (item is null) { return; } + onInstalledInfoButtonHit(item.Value); + }); + } + + public void PopulateInstalledModLists(bool forceRefreshEnabled = false, bool refreshDisabled = true) + { + bulkUpdateButton.Enabled = false; + bulkUpdateButton.ToolTip = ""; + ContentPackageManager.UpdateContentPackageList(); + + SwapDropdownValues(enabledCoreDropdown, + (p) => p.Name, + ContentPackageManager.CorePackages.ToArray(), + ContentPackageManager.EnabledPackages.Core!, + (p) => { }); + + void addRegularModToList(RegularPackage mod, GUIListBox list) + { + var modFrame = new GUIFrame(new RectTransform((1.0f, 0.08f), list.Content.RectTransform), + style: "ListBoxElement") + { + UserData = mod + }; + + var contextMenuHandler = new GUICustomComponent(new RectTransform(Vector2.Zero, modFrame.RectTransform), + onUpdate: (f, component) => + { + var parentList = modFrame.Parent?.Parent?.Parent as GUIListBox; //lovely jank :) + if (parentList is null) { return; } + if (GUI.MouseOn == modFrame && parentList.DraggedElement is null && PlayerInput.SecondaryMouseButtonClicked()) + { + if (!parentList.AllSelected.Contains(modFrame)) { parentList.Select(parentList.Content.GetChildIndex(modFrame)); } + static void noop() { } + + List contextMenuOptions = new List(); + if (ContentPackageManager.WorkshopPackages.Contains(mod)) + { + contextMenuOptions.Add( + new ContextMenuOption("ViewWorkshopModDetails".ToIdentifier(), isEnabled: true, onSelected: () => PrepareToShowModInfo(mod))); + } + + var labelConditions + = (parentList == enabledRegularModsList, parentList.AllSelected.Count > 1); + Identifier swapLabel = (labelConditions switch + { + (true, true) => "EnableSelectedWorkshopMods", + (true, false) => "EnableWorkshopMod", + (false, true) => "DisableSelectedWorkshopMods", + (false, false) => "DisableWorkshopMod" + }).ToIdentifier(); + + contextMenuOptions.Add(new ContextMenuOption(swapLabel, + isEnabled: true, onSelected: currentSwapFunc ?? noop)); + + var selectedMods = parentList.AllSelected.Select(it => it.UserData) + .OfType().ToArray(); + if (selectedMods.All(ContentPackageManager.LocalPackages.Contains) && selectedMods.Length > 1) + { + contextMenuOptions.Add(new ContextMenuOption("MergeSelectedMods".ToIdentifier(), isEnabled: true, + onSelected: () => ModMerger.AskMerge(selectedMods))); + } + + GUIButton? iconBtn(GUIComponent component) => component.GetChild()?.GetAllChildren().Last(); + if (selectedMods.All(ContentPackageManager.WorkshopPackages.Contains) + && parentList.AllSelected.All(c => iconBtn(c)?.Style?.Identifier == "WorkshopMenu.DownloadedIcon") + && selectedMods.Length > 0) + { + contextMenuOptions.Add(new ContextMenuOption( + (selectedMods.Length > 1 ? "UnsubscribeFromAllSelected" : "WorkshopItemUnsubscribe").ToIdentifier(), + isEnabled: true, + onSelected: () => + { + TaskPool.Add($"UnsubFromSelected", Task.WhenAll(selectedMods.Select(m => SteamManager.Workshop.GetItem(m.SteamWorkshopId))), + t => + { + if (!t.TryGetResult(out Steamworks.Ugc.Item?[] items)) { return; } + items.ForEach(it => + { + if (!(it is { } item)) { return; } + + item.Unsubscribe(); + SteamManager.Workshop.Uninstall(item); + PopulateInstalledModLists(); + }); + }); + })); + } + + GUIContextMenu.CreateContextMenu( + pos: PlayerInput.MousePosition, + header: ToolBox.LimitString(mod.Name, GUIStyle.SubHeadingFont, GUI.IntScale(300f)), + headerColor: null, + contextMenuOptions.ToArray()); + } + }); + + var frameContent = new GUILayoutGroup(new RectTransform((0.95f, 0.9f), modFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + RelativeSpacing = 0.02f + }; + + var dragIndicator = new GUIButton(new RectTransform((0.5f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: "GUIDragIndicator") + { + CanBeFocused = false + }; + + var modNameScissor = new GUIScissorComponent(new RectTransform((0.8f, 1.0f), frameContent.RectTransform)) + { + CanBeFocused = false + }; + var modName = new GUITextBlock(new RectTransform(Vector2.One, modNameScissor.Content.RectTransform), + text: mod.Name) + { + CanBeFocused = false + }; + if (mod.Errors.Any()) + { + CreateModErrorInfo(mod, modFrame, modName); + } + if (ContentPackageManager.LocalPackages.Contains(mod)) + { + var editButton = new GUIButton(new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", + style: "WorkshopMenu.EditButton") + { + OnClicked = (button, o) => + { + ToolBox.OpenFileWithShell(mod.Dir); + return false; + }, + ToolTip = TextManager.Get("OpenLocalModInExplorer") + }; + } + else if (ContentPackageManager.WorkshopPackages.Contains(mod)) + { + var infoButton = new GUIButton( + new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", + style: null) + { + CanBeSelected = false, + OnClicked = (button, o) => + { + PrepareToShowModInfo(mod); + return false; + } + }; + if (!SteamManager.IsInitialized) + { + infoButton.Enabled = false; + } + TaskPool.Add( + $"DetermineUpdateRequired{mod.SteamWorkshopId}", + mod.IsUpToDate(), + t => + { + if (!t.TryGetResult(out bool isUpToDate)) { return; } + + if (!isUpToDate) + { + infoButton.CanBeSelected = true; + infoButton.ApplyStyle(GUIStyle.ComponentStyles["WorkshopMenu.InfoButtonUpdate"]); + infoButton.ToolTip = TextManager.Get("ViewModDetailsUpdateAvailable"); + bulkUpdateButton.Enabled = true; + bulkUpdateButton.ToolTip = TextManager.Get("ModUpdatesAvailable"); + } + }); + } + } + + void addRegularModsToList(IEnumerable mods, GUIListBox list) + { + list.ClearChildren(); + foreach (var mod in mods) + { + addRegularModToList(mod, list); + } + } + + var enabledMods = + (forceRefreshEnabled || (enabledRegularModsList.Content.CountChildren + disabledRegularModsList.Content.CountChildren == 0) + ? ContentPackageManager.EnabledPackages.Regular + : enabledRegularModsList.Content.Children + .Select(c => c.UserData) + .OfType() + .Where(p => ContentPackageManager.RegularPackages.Contains(p))) + .ToArray(); + var disabledMods = ContentPackageManager.RegularPackages.Where(p => !enabledMods.Contains(p)); + + addRegularModsToList(enabledMods, enabledRegularModsList); + if (refreshDisabled) { addRegularModsToList(disabledMods, disabledRegularModsList); } + + TaskPool.Add( + $"DetermineWorkshopModIcons", + SteamManager.Workshop.GetPublishedItems(), + t => + { + if (!t.TryGetResult(out ISet items)) { return; } + var ids = items.Select(it => it.Id).ToHashSet(); + + foreach (var child in enabledRegularModsList.Content.Children + .Concat(disabledRegularModsList.Content.Children)) + { + var mod = child.UserData as RegularPackage; + if (mod is null || !ContentPackageManager.WorkshopPackages.Contains(mod)) { continue; } + + var btn = child.GetChild()?.GetAllChildren().Last(); + if (btn is null) { continue; } + if (btn.Style != null) { continue; } + + btn.ApplyStyle( + GUIStyle.GetComponentStyle( + ids.Contains(mod.SteamWorkshopId) + ? "WorkshopMenu.PublishedIcon" + : "WorkshopMenu.DownloadedIcon")); + btn.ToolTip = TextManager.Get( + ids.Contains(mod.SteamWorkshopId) + ? "PublishedWorkshopMod" + : "DownloadedWorkshopMod"); + btn.HoverCursor = CursorState.Default; + } + }); + + UpdateModListItemVisibility(); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs index dc9e38c57..7daabaacc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs @@ -151,7 +151,10 @@ namespace Barotrauma.Steam onDeselected: () => itemList?.Deselect(), out var select, out var deselect); - itemList = new GUIListBox(new RectTransform(Vector2.One, outerContainer.Content.RectTransform)); + itemList = new GUIListBox(new RectTransform(Vector2.One, outerContainer.Content.RectTransform)) + { + PlaySoundOnSelect = true, + }; itemList.RectTransform.SetAsFirstChild(); workshopItemList = itemList; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs new file mode 100644 index 000000000..df5e42de0 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs @@ -0,0 +1,256 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.IO; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + readonly struct ModListPreset + { + public const string SavePath = "ModLists"; + + public enum ModType + { + Vanilla, + Local, + Workshop + } + + public readonly string Name; + public readonly CorePackage CorePackage; + public readonly ImmutableArray RegularPackages; + + public ModListPreset(XDocument doc) + { + Name = doc.Root!.GetAttributeString("name", ""); + + CorePackage corePackage = ContentPackageManager.VanillaCorePackage!; + List regularPackages = new List(); + void addPkg(ContentPackage pkg) + { + if (pkg is CorePackage core) { corePackage = core; } + else if (pkg is RegularPackage reg) { regularPackages.Add(reg); } + } + + foreach (var element in doc.Root!.Elements()) + { + ModType modType = Enum.TryParse(element.Name.LocalName, ignoreCase: true, out var mt) ? mt : ModType.Local; + + switch (modType) + { + case ModType.Vanilla: + CorePackage = ContentPackageManager.VanillaCorePackage!; + break; + case ModType.Workshop: + { + var id = element.GetAttributeUInt64("id", 0); + var pkg = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == id); + if (id != 0 && pkg != null) + { + addPkg(pkg); + } + } + break; + case ModType.Local: + { + var name = element.GetAttributeString("name", ""); + var pkg = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.NameMatches(name)); + if (!name.IsNullOrEmpty() && pkg != null) + { + addPkg(pkg); + } + } + break; + } + } + + CorePackage = corePackage; + RegularPackages = regularPackages.ToImmutableArray(); + } + + public ModListPreset(string name, CorePackage corePackage, IReadOnlyList regularPackages) + { + Name = name; + CorePackage = corePackage; + RegularPackages = regularPackages.ToImmutableArray(); + } + + public RichString GetTooltip() + { + LocalizedString retVal = $"‖color:gui.orange‖{Name}‖end‖" //TODO: we need a RichString builder + + "\n " + TextManager.AddPunctuation(':', TextManager.Get("CorePackage")) + + "\n - " + CorePackage.Name; + if (RegularPackages.Any()) + { + retVal += "\n " + TextManager.AddPunctuation(':', TextManager.Get("RegularPackages")) + + "\n - " + + LocalizedString.Join("\n - ", RegularPackages.Select(p => (LocalizedString)p.Name)); + } + + return RichString.Rich(retVal); + } + + public void Save() + { + XDocument newDoc = new XDocument(); + XElement newRoot = new XElement("mods", new XAttribute("name", Name)); + newDoc.Add(newRoot); + + ModType determineType(ContentPackage pkg) + { + if (pkg == ContentPackageManager.VanillaCorePackage) { return ModType.Vanilla; } + if (ContentPackageManager.WorkshopPackages.Contains(pkg)) { return ModType.Workshop; } + return ModType.Local; + } + void writePkgElem(ContentPackage pkg) + { + var pkgType = determineType(pkg); + var pkgElem = new XElement(pkgType.ToString()); + switch (pkgType) + { + case ModType.Workshop: + pkgElem.SetAttributeValue("name", pkg.Name); + pkgElem.SetAttributeValue("id", pkg.SteamWorkshopId.ToString()); + break; + case ModType.Local: + pkgElem.SetAttributeValue("name", pkg.Name); + break; + } + newRoot.Add(pkgElem); + } + writePkgElem(CorePackage); + RegularPackages.ForEach(writePkgElem); + + if (!Directory.Exists(SavePath)) { Directory.CreateDirectory(SavePath); } + newDoc.SaveSafe(Path.Combine(SavePath, ToolBox.RemoveInvalidFileNameChars($"{Name}.xml"))); + } + } +} + +namespace Barotrauma.Steam +{ + sealed partial class MutableWorkshopMenu : WorkshopMenu + { + private bool OpenLoadPreset(GUIButton _, object __) + { + OpenLoadPreset(); + return false; + } + + private void OpenLoadPreset() + { + var msgBox = new GUIMessageBox( + TextManager.Get("LoadModListPresetHeader"), + "", + buttons: new [] { TextManager.Get("Load"), TextManager.Get("Cancel") }, + relativeSize: (0.4f, 0.6f)); + + var presetListBox = new GUIListBox(new RectTransform((1.0f, 0.7f), msgBox.Content.RectTransform)); + + (string Path, XDocument? Doc) tryLoadXml(string path) + => (path, XMLExtensions.TryLoadXml(path)); + + var presets = Directory.Exists(ModListPreset.SavePath) + ? Directory.GetFiles(ModListPreset.SavePath) + .Select(tryLoadXml) + .Where(d => d.Doc != null) + .ToArray() + : Array.Empty<(string Path, XDocument? Doc)>(); + + foreach (var doc in presets) + { + ModListPreset preset = new ModListPreset(doc.Doc!); + var presetFrame = new GUIFrame(new RectTransform((1.0f, 0.09f), presetListBox.Content.RectTransform), + style: "ListBoxElement") + { + UserData = preset, + ToolTip = preset.GetTooltip() + }; + new GUITextBlock(new RectTransform(Vector2.One, presetFrame.RectTransform), preset.Name) + { + CanBeFocused = false + }; + var deleteBtn + = new GUIButton(new RectTransform((0.2f, 1.0f), presetFrame.RectTransform, Anchor.CenterRight), + TextManager.Get("Delete"), style: "GUIButtonSmall") + { + OnClicked = (button, o) => + { + File.Delete(doc.Path); + presetListBox.Content.RemoveChild(presetFrame); + return false; + } + }; + } + + msgBox.Buttons[0].OnClicked = (button, o) => + { + if (presetListBox.SelectedData is ModListPreset preset) + { + var allChildren = enabledRegularModsList.Content.Children + .Concat(disabledRegularModsList.Content.Children) + .ToArray(); + enabledRegularModsList.ClearChildren(); + disabledRegularModsList.ClearChildren(); + var toEnable = + allChildren.Where(c => c.UserData is RegularPackage p + && preset.RegularPackages.Contains(p)) + .OrderBy(c => c.UserData is RegularPackage p ? preset.RegularPackages.IndexOf(p) : int.MaxValue) + .ToArray(); + var toDisable = allChildren.Where(c => !toEnable.Contains(c)).ToArray(); + toEnable.ForEach(c => c.RectTransform.Parent = enabledRegularModsList.Content.RectTransform); + toDisable.ForEach(c => c.RectTransform.Parent = disabledRegularModsList.Content.RectTransform); + + enabledCoreDropdown.SelectItem(preset.CorePackage); + } + msgBox.Close(); + return false; + }; + msgBox.Buttons[1].OnClicked = msgBox.Close; + } + + private bool OpenSavePreset(GUIButton _, object __) + { + OpenSavePreset(); + return false; + } + + private void OpenSavePreset() + { + var msgBox = new GUIMessageBox( + TextManager.Get("SaveModListPresetHeader"), + "", + buttons: new [] { TextManager.Get("Save"), TextManager.Get("Cancel") }, + relativeSize: (0.4f, 0.2f)); + + var nameBox = new GUITextBox(new RectTransform((1.0f, 0.3f), msgBox.Content.RectTransform), ""); + + msgBox.Buttons[0].OnClicked = (button, o) => + { + if (nameBox.Text.IsNullOrEmpty()) + { + nameBox.Flash(GUIStyle.Red); + return false; + } + + if (enabledCoreDropdown.SelectedData is CorePackage corePackage) + { + ModListPreset preset = new ModListPreset(nameBox.Text, + corePackage, + enabledRegularModsList.Content.Children + .Select(c => c.UserData) + .OfType().ToArray()); + preset.Save(); + } + msgBox.Close(); + return false; + }; + msgBox.Buttons[1].OnClicked = msgBox.Close; + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs index 49632af2b..c8563eb3c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs @@ -3,9 +3,9 @@ using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using System.Threading; +using System.Threading.Tasks; using ItemOrPackage = Barotrauma.Either; namespace Barotrauma.Steam @@ -20,20 +20,20 @@ namespace Barotrauma.Steam Publish } + private enum Filter + { + ShowLocal, + ShowWorkshop, + ShowPublished, + ShowOnlySubs, + ShowOnlyItemAssemblies + } + private readonly GUILayoutGroup tabber; private readonly Dictionary tabContents; private readonly GUIFrame contentFrame; - private CorePackage EnabledCorePackage => enabledCoreDropdown.SelectedData as CorePackage ?? throw new Exception("Valid core package not selected"); - - private readonly GUIDropDown enabledCoreDropdown; - private readonly GUIListBox enabledRegularModsList; - private readonly GUIListBox disabledRegularModsList; - private readonly Action onInstalledInfoButtonHit; - private readonly GUITextBox modsListFilter; - private readonly GUIButton bulkUpdateButton; - private CancellationTokenSource taskCancelSrc = new CancellationTokenSource(); private readonly HashSet itemThumbnails = new HashSet(); @@ -41,7 +41,7 @@ namespace Barotrauma.Steam private readonly GUIListBox selfModsList; private uint memSubscribedModCount = 0; - + public MutableWorkshopMenu(GUIFrame parent) : base(parent) { var mainLayout @@ -62,6 +62,7 @@ namespace Barotrauma.Steam out disabledRegularModsList, out onInstalledInfoButtonHit, out modsListFilter, + out modsListFilterTickboxes, out bulkUpdateButton); CreatePopularModsTab(out popularModsList); CreatePublishTab(out selfModsList); @@ -69,45 +70,6 @@ namespace Barotrauma.Steam SelectTab(Tab.InstalledMods); } - private void UpdateSubscribedModInstalls() - { - if (!SteamManager.IsInitialized) { return; } - - uint numSubscribedMods = SteamManager.GetNumSubscribedItems(); - if (numSubscribedMods == memSubscribedModCount) { return; } - memSubscribedModCount = numSubscribedMods; - - var subscribedIds = SteamManager.GetSubscribedItems().ToHashSet(); - var installedIds = ContentPackageManager.WorkshopPackages.Select(p => p.SteamWorkshopId).ToHashSet(); - foreach (var id in subscribedIds.Where(id2 => !installedIds.Contains(id2))) - { - Steamworks.Ugc.Item item = new Steamworks.Ugc.Item(id); - if (!item.IsDownloading && !SteamManager.Workshop.IsInstalling(item)) - { - SteamManager.Workshop.DownloadModThenEnqueueInstall(item); - } - } - - TaskPool.Add("RemoveUnsubscribedItems", SteamManager.Workshop.GetPublishedItems(), t => - { - if (!t.TryGetResult(out ISet publishedItems)) { return; } - - var allRequiredInstalled = subscribedIds.Union(publishedItems.Select(it => it.Id)).ToHashSet(); - bool needsRefresh = false; - foreach (var id in installedIds.Where(id2 => !allRequiredInstalled.Contains(id2))) - { - Steamworks.Ugc.Item item = new Steamworks.Ugc.Item(id); - SteamManager.Workshop.Uninstall(item); - needsRefresh = true; - } - - if (needsRefresh) - { - PopulateInstalledModLists(); - } - }); - } - private void SwitchContent(GUIFrame newContent) { contentFrame.Children.ForEach(c => c.Visible = false); @@ -161,460 +123,6 @@ namespace Barotrauma.Steam return content; } - private static (GUILayoutGroup Left, GUIFrame center, GUILayoutGroup Right) CreateSidebars( - GUIComponent parent, - float leftWidth = 0.3875f, - float centerWidth = 0.025f, - float rightWidth = 0.5875f, - bool split = false, - float height = 1.0f) - { - GUILayoutGroup layout = new GUILayoutGroup(new RectTransform((1.0f, height), parent.RectTransform), isHorizontal: true); - GUILayoutGroup left = new GUILayoutGroup(new RectTransform((leftWidth, 1.0f), layout.RectTransform), isHorizontal: false); - var center = new GUIFrame(new RectTransform((centerWidth, 1.0f), layout.RectTransform), style: null); - if (split) - { - new GUICustomComponent(new RectTransform(Vector2.One, center.RectTransform), - onDraw: (sb, c) => - { - sb.DrawLine((c.Rect.Center.X, c.Rect.Top), (c.Rect.Center.X, c.Rect.Bottom), GUIStyle.TextColorDim, 2f); - }); - } - GUILayoutGroup right = new GUILayoutGroup(new RectTransform((rightWidth, 1.0f), layout.RectTransform), isHorizontal: false); - return (left, center, right); - } - - private void HandleDraggingAcrossModLists(GUIListBox from, GUIListBox to) - { - if (to.Rect.Contains(PlayerInput.MousePosition) && from.DraggedElement != null) - { - //move the dragged elements to the index determined previously - var draggedElement = from.DraggedElement; - - var selected = from.AllSelected.ToList(); - selected.Sort((a, b) => from.Content.GetChildIndex(a) - from.Content.GetChildIndex(b)); - - float oldCount = to.Content.CountChildren; - float newCount = oldCount + selected.Count; - - var offset = draggedElement.RectTransform.AbsoluteOffset; - offset += from.Content.Rect.Location; - offset -= to.Content.Rect.Location; - - for (int i = 0; i < selected.Count; i++) - { - var c = selected[i]; - c.Parent.RemoveChild(c); - c.RectTransform.Parent = to.Content.RectTransform; - c.RectTransform.RepositionChildInHierarchy((int)oldCount+i); - } - - from.DraggedElement = null; - from.Deselect(); - from.RecalculateChildren(); - from.RectTransform.RecalculateScale(true); - to.RecalculateChildren(); - to.RectTransform.RecalculateScale(true); - to.Select(selected); - - //recalculate the dragged element's offset so it doesn't jump around - draggedElement.RectTransform.AbsoluteOffset = offset; - - to.DraggedElement = draggedElement; - - to.BarScroll *= (oldCount / newCount); - } - } - - private Action? currentSwapFunc = null; - - private void SetSwapFunc(GUIListBox from, GUIListBox to) - { - currentSwapFunc = () => - { - to.Deselect(); - var selected = from.AllSelected.ToArray(); - foreach (var frame in selected) - { - frame.Parent.RemoveChild(frame); - frame.RectTransform.Parent = to.Content.RectTransform; - } - from.RecalculateChildren(); - from.RectTransform.RecalculateScale(true); - to.RecalculateChildren(); - to.RectTransform.RecalculateScale(true); - to.Select(selected); - }; - } - - private void CreateInstalledModsTab( - out GUIDropDown enabledCoreDropdown, - out GUIListBox enabledRegularModsList, - out GUIListBox disabledRegularModsList, - out Action onInstalledInfoButtonHit, - out GUITextBox modsListFilter, - out GUIButton bulkUpdateButton) - { - GUIFrame content = CreateNewContentFrame(Tab.InstalledMods); - - CreateWorkshopItemDetailContainer( - content, - out var outerContainer, - onSelected: (itemOrPackage, selectedFrame) => - { - if (itemOrPackage.TryGet(out Steamworks.Ugc.Item item)) { PopulateFrameWithItemInfo(item, selectedFrame); } - }, - onDeselected: () => PopulateInstalledModLists(), - out onInstalledInfoButtonHit, out var deselect); - - GUILayoutGroup mainLayout = - new GUILayoutGroup(new RectTransform(Vector2.One, outerContainer.Content.RectTransform), childAnchor: Anchor.TopCenter); - mainLayout.RectTransform.SetAsFirstChild(); - - var (topLeft, _, topRight) = CreateSidebars(mainLayout, centerWidth: 0.05f, leftWidth: 0.475f, rightWidth: 0.475f, height: 0.13f); - topLeft.Stretch = true; - Label(topLeft, TextManager.Get("enabledcore"), GUIStyle.SubHeadingFont, heightScale: 1.0f); - enabledCoreDropdown = Dropdown(topLeft, - (p) => p.Name, - ContentPackageManager.CorePackages.ToArray(), - ContentPackageManager.EnabledPackages.Core!, - (p) => { }, - heightScale: 1.0f / 13.0f); - Label(topLeft, "", GUIStyle.SubHeadingFont, heightScale: 1.0f); - topRight.ChildAnchor = Anchor.CenterLeft; - - var topRightButtons = new GUILayoutGroup(new RectTransform((1.0f, 0.5f), topRight.RectTransform), - isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - RelativeSpacing = 0.05f - }; - - void padTopRight(float width=1.0f) - { - new GUIFrame(new RectTransform((width, 1.0f), topRightButtons.RectTransform), style: null); - } - - padTopRight(); - //TODO: put stuff here - padTopRight(width: 3.0f); - var refreshListsButton - = new GUIButton( - new RectTransform(Vector2.One, topRightButtons.RectTransform, scaleBasis: ScaleBasis.BothHeight), - text: "", style: "GUIReloadButton") - { - OnClicked = (b, o) => - { - PopulateInstalledModLists(); - return false; - }, - ToolTip = TextManager.Get("RefreshModLists") - }; - bulkUpdateButton - = new GUIButton( - new RectTransform(Vector2.One, topRightButtons.RectTransform, scaleBasis: ScaleBasis.BothHeight), - text: "", style: "GUIUpdateButton") - { - OnClicked = (b, o) => - { - BulkDownloader.PrepareUpdates(); - return false; - }, - Enabled = false - }; - padTopRight(width: 0.1f); - - var (left, center, right) = CreateSidebars(mainLayout, centerWidth: 0.05f, leftWidth: 0.475f, rightWidth: 0.475f, height: 0.8f); - right.ChildAnchor = Anchor.TopRight; - - //enabled mods - Label(left, TextManager.Get("enabledregular"), GUIStyle.SubHeadingFont); - var enabledModsList = new GUIListBox(new RectTransform((1.0f, 0.93f), left.RectTransform)) - { - CurrentDragMode = GUIListBox.DragMode.DragOutsideBox, - CurrentSelectMode = GUIListBox.SelectMode.RequireShiftToSelectMultiple, - HideDraggedElement = true - }; - enabledRegularModsList = enabledModsList; - - //disabled mods - Label(right, TextManager.Get("disabledregular"), GUIStyle.SubHeadingFont); - var disabledModsList = new GUIListBox(new RectTransform((1.0f, 0.93f), right.RectTransform)) - { - CurrentDragMode = GUIListBox.DragMode.DragOutsideBox, - CurrentSelectMode = GUIListBox.SelectMode.RequireShiftToSelectMultiple, - HideDraggedElement = true - }; - disabledRegularModsList = disabledModsList; - - var centerButton = - new GUIButton( - new RectTransform(Vector2.One * 0.95f, center.RectTransform, scaleBasis: ScaleBasis.BothWidth, - anchor: Anchor.Center), - style: "GUIButtonToggleLeft") - { - Visible = false, - OnClicked = (button, o) => - { - currentSwapFunc?.Invoke(); - return false; - } - }; - - enabledModsList.OnSelected = (frame, o) => - { - disabledModsList.Deselect(); - - centerButton.Visible = true; - centerButton.ApplyStyle(GUIStyle.GetComponentStyle("GUIButtonToggleRight")); - - SetSwapFunc(enabledModsList, disabledModsList); - - return true; - }; - disabledModsList.OnSelected = (frame, o) => - { - enabledModsList.Deselect(); - - centerButton.Visible = true; - centerButton.ApplyStyle(GUIStyle.GetComponentStyle("GUIButtonToggleLeft")); - - SetSwapFunc(disabledModsList, enabledModsList); - - return true; - }; - - var searchBox = CreateSearchBox(mainLayout, width: 0.5f); - modsListFilter = searchBox; - - new GUICustomComponent(new RectTransform(Vector2.Zero, content.RectTransform), - onUpdate: (f, component) => - { - HandleDraggingAcrossModLists(enabledModsList, disabledModsList); - HandleDraggingAcrossModLists(disabledModsList, enabledModsList); - if (PlayerInput.PrimaryMouseButtonClicked() - && !GUI.IsMouseOn(enabledModsList) - && !GUI.IsMouseOn(disabledModsList) - && GUIContextMenu.CurrentContextMenu is null) - { - enabledModsList.Deselect(); - disabledModsList.Deselect(); - } - else if (!PlayerInput.IsCtrlDown() && !PlayerInput.IsShiftDown() && PlayerInput.DoubleClicked()) - { - currentSwapFunc?.Invoke(); - } - }, - onDraw: (spriteBatch, component) => - { - enabledModsList.DraggedElement?.DrawManually(spriteBatch, true, true); - disabledModsList.DraggedElement?.DrawManually(spriteBatch, true, true); - }); - } - - protected override void UpdateModListItemVisibility() - { - string str = modsListFilter.Text; - enabledRegularModsList.Content.Children.Concat(disabledRegularModsList.Content.Children) - .ForEach(c => c.Visible = str.IsNullOrWhiteSpace() - || (c.UserData is ContentPackage p - && p.Name.Contains(str, StringComparison.OrdinalIgnoreCase))); - } - - private void PrepareToShowModInfo(ContentPackage mod) - { - TaskPool.Add($"PrepareToShow{mod.SteamWorkshopId}Info", SteamManager.Workshop.GetItem(mod.SteamWorkshopId), - t => - { - if (!t.TryGetResult(out Steamworks.Ugc.Item? item)) { return; } - if (item is null) { return; } - onInstalledInfoButtonHit(item.Value); - }); - } - - public void PopulateInstalledModLists(bool forceRefreshEnabled = false, bool refreshDisabled = true) - { - bulkUpdateButton.Enabled = false; - bulkUpdateButton.ToolTip = ""; - ContentPackageManager.UpdateContentPackageList(); - - SwapDropdownValues(enabledCoreDropdown, - (p) => p.Name, - ContentPackageManager.CorePackages.ToArray(), - ContentPackageManager.EnabledPackages.Core!, - (p) => { }); - - void addRegularModToList(RegularPackage mod, GUIListBox list) - { - var modFrame = new GUIFrame(new RectTransform((1.0f, 0.08f), list.Content.RectTransform), - style: "ListBoxElement") - { - UserData = mod - }; - - var contextMenuHandler = new GUICustomComponent(new RectTransform(Vector2.Zero, modFrame.RectTransform), - onUpdate: (f, component) => - { - var parentList = modFrame.Parent?.Parent?.Parent as GUIListBox; //lovely jank :) - if (parentList is null) { return; } - if (GUI.MouseOn == modFrame && parentList.DraggedElement is null && PlayerInput.SecondaryMouseButtonClicked()) - { - if (!parentList.AllSelected.Contains(modFrame)) { parentList.Select(parentList.Content.GetChildIndex(modFrame)); } - static void noop() { } - - List contextMenuOptions = new List(); - if (ContentPackageManager.WorkshopPackages.Contains(mod)) - { - contextMenuOptions.Add( - new ContextMenuOption("ViewWorkshopModDetails".ToIdentifier(), isEnabled: true, onSelected: () => PrepareToShowModInfo(mod))); - } - - Identifier swapLabel - = ((parentList == enabledRegularModsList ? "Disable" : "Enable") - + (parentList.AllSelected.Count > 1 ? "SelectedWorkshopMods" : "WorkshopMod")) - .ToIdentifier(); - - contextMenuOptions.Add(new ContextMenuOption(swapLabel, - isEnabled: true, onSelected: currentSwapFunc ?? noop)); - - GUIContextMenu.CreateContextMenu( - pos: PlayerInput.MousePosition, - header: ToolBox.LimitString(mod.Name, GUIStyle.SubHeadingFont, GUI.IntScale(300f)), - headerColor: null, - contextMenuOptions.ToArray()); - } - }); - - var frameContent = new GUILayoutGroup(new RectTransform((0.95f, 0.9f), modFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - var dragIndicator = new GUIButton(new RectTransform((0.5f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight), - style: "GUIDragIndicator") - { - CanBeFocused = false - }; - - var modNameScissor = new GUIScissorComponent(new RectTransform((0.8f, 1.0f), frameContent.RectTransform)) - { - CanBeFocused = false - }; - var modName = new GUITextBlock(new RectTransform(Vector2.One, modNameScissor.Content.RectTransform), - text: mod.Name) - { - CanBeFocused = false - }; - if (mod.Errors.Any()) - { - CreateModErrorInfo(mod, modFrame, modName); - } - if (ContentPackageManager.LocalPackages.Contains(mod)) - { - var editButton = new GUIButton(new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", - style: "WorkshopMenu.EditButton") - { - OnClicked = (button, o) => - { - ToolBox.OpenFileWithShell(mod.Dir); - return false; - }, - ToolTip = TextManager.Get("OpenLocalModInExplorer") - }; - } - else if (ContentPackageManager.WorkshopPackages.Contains(mod)) - { - var infoButton = new GUIButton( - new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", - style: null) - { - CanBeSelected = false, - OnClicked = (button, o) => - { - PrepareToShowModInfo(mod); - return false; - } - }; - if (!SteamManager.IsInitialized) - { - infoButton.Enabled = false; - } - TaskPool.Add( - $"DetermineUpdateRequired{mod.SteamWorkshopId}", - mod.IsUpToDate(), - t => - { - if (!t.TryGetResult(out bool isUpToDate)) { return; } - - if (!isUpToDate) - { - infoButton.CanBeSelected = true; - infoButton.ApplyStyle(GUIStyle.ComponentStyles["WorkshopMenu.InfoButtonUpdate"]); - infoButton.ToolTip = TextManager.Get("ViewModDetailsUpdateAvailable"); - bulkUpdateButton.Enabled = true; - bulkUpdateButton.ToolTip = TextManager.Get("ModUpdatesAvailable"); - } - }); - } - } - - void addRegularModsToList(IEnumerable mods, GUIListBox list) - { - list.ClearChildren(); - foreach (var mod in mods) - { - addRegularModToList(mod, list); - } - } - - var enabledMods = - (forceRefreshEnabled || (enabledRegularModsList.Content.CountChildren + disabledRegularModsList.Content.CountChildren == 0) - ? ContentPackageManager.EnabledPackages.Regular - : enabledRegularModsList.Content.Children - .Select(c => c.UserData) - .OfType() - .Where(p => ContentPackageManager.RegularPackages.Contains(p))) - .ToArray(); - var disabledMods = ContentPackageManager.RegularPackages.Where(p => !enabledMods.Contains(p)); - - addRegularModsToList(enabledMods, enabledRegularModsList); - if (refreshDisabled) { addRegularModsToList(disabledMods, disabledRegularModsList); } - - TaskPool.Add( - $"DetermineWorkshopModIcons", - SteamManager.Workshop.GetPublishedItems(), - t => - { - if (!t.TryGetResult(out ISet items)) { return; } - var ids = items.Select(it => it.Id).ToHashSet(); - - foreach (var child in enabledRegularModsList.Content.Children - .Concat(disabledRegularModsList.Content.Children)) - { - var mod = child.UserData as RegularPackage; - if (mod is null || !ContentPackageManager.WorkshopPackages.Contains(mod)) { continue; } - - var btn = child.GetChild()?.GetAllChildren().Last(); - if (btn is null) { continue; } - if (btn.Style != null) { continue; } - - btn.ApplyStyle( - GUIStyle.GetComponentStyle( - ids.Contains(mod.SteamWorkshopId) - ? "WorkshopMenu.PublishedIcon" - : "WorkshopMenu.DownloadedIcon")); - btn.ToolTip = TextManager.Get( - ids.Contains(mod.SteamWorkshopId) - ? "PublishedWorkshopMod" - : "DownloadedWorkshopMod"); - btn.HoverCursor = CursorState.Default; - } - }); - - UpdateModListItemVisibility(); - } - private void CreatePopularModsTab(out GUIListBox popularModsList) { GUIFrame content = CreateNewContentFrame(Tab.PopularMods); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs index e803a86e2..cbe5fca4d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs @@ -106,10 +106,8 @@ namespace Barotrauma.Steam => new GUIFrame(new RectTransform(Vector2.Zero, parent.RectTransform), style: null) { UserData = new ActionCarrier(id, action) }; - protected GUITextBox CreateSearchBox(GUILayoutGroup mainLayout, float width = 1.0f, float heightScale = 1.0f) + protected GUITextBox CreateSearchBox(RectTransform searchRectT) { - var searchRectT = NewItemRectT(mainLayout, heightScale: heightScale); - searchRectT.RelativeSize = (width, searchRectT.RelativeSize.Y); var searchHolder = new GUIFrame(searchRectT, style: null); var searchBox = new GUITextBox(new RectTransform(Vector2.One, searchHolder.RectTransform), "", createClearButton: true); var searchTitle = new GUITextBlock(new RectTransform(Vector2.One, searchHolder.RectTransform) {Anchor = Anchor.TopLeft}, @@ -142,7 +140,8 @@ namespace Barotrauma.Steam const int maxErrorsToShow = 5; nameText.TextColor = GUIStyle.Red; uiElement.ToolTip = - TextManager.GetWithVariable("contentpackagehaserrors", "[packagename]", mod.Name) + '\n' + string.Join('\n', mod.Errors.Take(maxErrorsToShow).Select(e => e.error)); + TextManager.GetWithVariable("contentpackagehaserrors", "[packagename]", mod.Name) + + '\n' + string.Join('\n', mod.Errors.Take(maxErrorsToShow).Select(e => e.Message)); if (mod.Errors.Count() > maxErrorsToShow) { uiElement.ToolTip += '\n' + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (mod.Errors.Count() - maxErrorsToShow).ToString()); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/WorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/WorkshopMenu.cs index ebe59aaf8..003aab946 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/WorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/WorkshopMenu.cs @@ -1,3 +1,5 @@ +using System; + #nullable enable namespace Barotrauma.Steam @@ -7,5 +9,8 @@ namespace Barotrauma.Steam public WorkshopMenu(GUIFrame parent) { } protected abstract void UpdateModListItemVisibility(); + + protected bool ModNameMatches(ContentPackage p, string query) + => p.Name.Contains(query, StringComparison.OrdinalIgnoreCase); } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index f691db09d..2763d0211 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,12 +6,13 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.2.0 + 0.18.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico Debug;Release;Unstable + true ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 7842466de..d129a0104 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,12 +6,13 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.2.0 + 0.18.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico Debug;Release;Unstable + true ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index fa39bbdbb..1f520e768 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,13 +6,14 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.2.0 + 0.18.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico Debug;Release;Unstable app.manifest + true ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index dec7d455e..77e6d7d8a 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,12 +6,13 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.2.0 + 0.18.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable + true ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 6d151701f..c68077f24 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,12 +6,13 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.2.0 + 0.18.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable + true ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index c58fb75c8..9ad728290 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -647,6 +647,7 @@ namespace Barotrauma { msg.Write(false); } + msg.Write(HumanPrefabHealthMultiplier); msg.Write(Wallet.Balance); msg.WriteRangedInteger(Wallet.RewardDistribution, 0, 100); msg.Write((byte)TeamID); diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 61c141015..a9a05313a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1676,7 +1676,7 @@ namespace Barotrauma GameMain.Server.SendConsoleMessage("No campaign active.", client, Color.Red); return; } - mpCampaign.LastUpdateID++; + mpCampaign.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions); GameMain.GameSession.Map.AllowDebugTeleport = !GameMain.GameSession.Map.AllowDebugTeleport; NewMessage(client.Name + (GameMain.GameSession.Map.AllowDebugTeleport ? " enabled" : " disabled") + " teleportation on the campaign map.", Color.White); GameMain.Server.SendConsoleMessage((GameMain.GameSession.Map.AllowDebugTeleport ? "Enabled" : "Disabled") + " teleportation on the campaign map.", client); @@ -2274,7 +2274,6 @@ namespace Barotrauma Wallet wallet = targetCharacter is null ? campaign.Bank : targetCharacter.Wallet; wallet.Give(money); GameAnalyticsManager.AddMoneyGainedEvent(money, GameAnalyticsManager.MoneySource.Cheat, "console"); - campaign.LastUpdateID++; } else { diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 89ed9f0dc..a562798b2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -37,7 +37,7 @@ namespace Barotrauma { if (forceMapUI == value) { return; } forceMapUI = value; - LastUpdateID++; + IncrementLastUpdateIdForFlag(NetFlags.MapAndMissions); } } @@ -71,11 +71,43 @@ namespace Barotrauma get { return ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition"); } } - public static void StartNewCampaign(string savePath, string subPath, string seed, CampaignSettings settings) + private bool purchasedHullRepairs, purchasedLostShuttles, purchasedItemRepairs; + public override bool PurchasedHullRepairs + { + get { return purchasedHullRepairs; } + set + { + if (purchasedHullRepairs == value) { return; } + purchasedHullRepairs = value; + IncrementLastUpdateIdForFlag(NetFlags.Misc); + } + } + public override bool PurchasedLostShuttles + { + get { return purchasedLostShuttles; } + set + { + if (purchasedLostShuttles == value) { return; } + purchasedLostShuttles = value; + IncrementLastUpdateIdForFlag(NetFlags.Misc); + } + } + public override bool PurchasedItemRepairs + { + get { return purchasedItemRepairs; } + set + { + if (purchasedItemRepairs == value) { return; } + purchasedItemRepairs = value; + IncrementLastUpdateIdForFlag(NetFlags.Misc); + } + } + + public static void StartNewCampaign(string savePath, string subPath, string seed, CampaignSettings startingSettings) { if (string.IsNullOrWhiteSpace(savePath)) { return; } - GameMain.GameSession = new GameSession(new SubmarineInfo(subPath), savePath, GameModePreset.MultiPlayerCampaign, settings, seed); + GameMain.GameSession = new GameSession(new SubmarineInfo(subPath), savePath, GameModePreset.MultiPlayerCampaign, startingSettings, seed); GameMain.NetLobbyScreen.ToggleCampaignMode(true); SaveUtil.SaveGame(GameMain.GameSession.SavePath); @@ -158,7 +190,7 @@ namespace Barotrauma public override void Start() { base.Start(); - lastUpdateID++; + IncrementAllLastUpdateIds(); } private static bool IsOwner(Client client) => client != null && client.Connection == GameMain.Server.OwnerConnection; @@ -274,7 +306,7 @@ namespace Barotrauma protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults) { - lastUpdateID++; + IncrementAllLastUpdateIds(); switch (transitionType) { @@ -321,6 +353,7 @@ namespace Barotrauma yield return CoroutineStatus.Running; LeaveUnconnectedSubs(leavingSub); NextLevel = newLevel; + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); SaveUtil.SaveGame(GameMain.GameSession.SavePath); } else @@ -329,7 +362,7 @@ namespace Barotrauma GameMain.Server.EndGame(TransitionType.None, wasSaved: false); LoadCampaign(GameMain.GameSession.SavePath); LastSaveID++; - LastUpdateID++; + IncrementAllLastUpdateIds(); yield return CoroutineStatus.Success; } @@ -360,14 +393,14 @@ namespace Barotrauma } partial void InitProjSpecific() - { - CargoManager.OnItemsInBuyCrateChanged += () => { LastUpdateID++; }; - CargoManager.OnPurchasedItemsChanged += () => { LastUpdateID++; }; - CargoManager.OnSoldItemsChanged += () => { LastUpdateID++; }; - UpgradeManager.OnUpgradesChanged += () => { LastUpdateID++; }; - Map.OnLocationSelected += (loc, connection) => { LastUpdateID++; }; - Map.OnMissionsSelected += (loc, mission) => { LastUpdateID++; }; - Reputation.OnAnyReputationValueChanged += () => { LastUpdateID++; }; + { + CargoManager.OnItemsInBuyCrateChanged += () => { IncrementLastUpdateIdForFlag(NetFlags.ItemsInBuyCrate); }; + CargoManager.OnPurchasedItemsChanged += () => { IncrementLastUpdateIdForFlag(NetFlags.PurchasedItems); }; + CargoManager.OnSoldItemsChanged += () => { IncrementLastUpdateIdForFlag(NetFlags.SoldItems); }; + UpgradeManager.OnUpgradesChanged += () => { IncrementLastUpdateIdForFlag(NetFlags.UpgradeManager); }; + Map.OnLocationSelected += (loc, connection) => { IncrementLastUpdateIdForFlag(NetFlags.MapAndMissions); }; + Map.OnMissionsSelected += (loc, mission) => { IncrementLastUpdateIdForFlag(NetFlags.MapAndMissions); }; + Reputation.OnAnyReputationValueChanged += () => { IncrementLastUpdateIdForFlag(NetFlags.Reputation); }; //increment save ID so clients know they're lacking the most up-to-date save file LastSaveID++; @@ -388,6 +421,7 @@ namespace Barotrauma discardedCharacters.Add(data); } characterData.Remove(data); + IncrementLastUpdateIdForFlag(NetFlags.CharacterInfo); } } } @@ -402,6 +436,7 @@ namespace Barotrauma characterData.RemoveAll(cd => cd.MatchesClient(client)); var data = new CharacterCampaignData(client); characterData.Add(data); + IncrementLastUpdateIdForFlag(NetFlags.CharacterInfo); return data; } @@ -413,6 +448,7 @@ namespace Barotrauma var matchingData = GetClientCharacterData(client); if (matchingData != null) { client.CharacterInfo = matchingData.CharacterInfo; } } + IncrementLastUpdateIdForFlag(NetFlags.CharacterInfo); } public Dictionary GetAssignedJobs(IEnumerable connectedClients) @@ -517,127 +553,187 @@ namespace Barotrauma base.End(transitionType); } + private bool IsFlagRequired(Client c, NetFlags flag) + => !c.LastRecvCampaignUpdate.TryGetValue(flag, out var id) || NetIdUtils.IdMoreRecent(GetLastUpdateIdForFlag(flag), id); + public void ServerWrite(IWriteMessage msg, Client c) { System.Diagnostics.Debug.Assert(map.Locations.Count < UInt16.MaxValue); - Reputation reputation = Map?.CurrentLocation?.Reputation; + NetFlags requiredFlags = lastUpdateID.Keys.Where(k => IsFlagRequired(c, k)).Aggregate((NetFlags)0, (f1, f2) => f1 | f2); + + msg.Write((UInt16)requiredFlags); msg.Write(IsFirstRound); msg.Write(CampaignID); - msg.Write(lastUpdateID); msg.Write(lastSaveID); msg.Write(map.Seed); - msg.Write(map.CurrentLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.CurrentLocationIndex); - msg.Write(map.SelectedLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.SelectedLocationIndex); - - var selectedMissionIndices = map.GetSelectedMissionIndices(); - msg.Write((byte)selectedMissionIndices.Count()); - foreach (int selectedMissionIndex in selectedMissionIndices) + + if (requiredFlags.HasFlag(NetFlags.Misc)) { - msg.Write((byte)selectedMissionIndex); + msg.Write(GetLastUpdateIdForFlag(NetFlags.Misc)); + msg.Write(PurchasedHullRepairs); + msg.Write(PurchasedItemRepairs); + msg.Write(PurchasedLostShuttles); } - var subList = GameMain.NetLobbyScreen.GetSubList(); - List ownedSubmarineIndices = new List(); - for (int i = 0; i < subList.Count; i++) + if (requiredFlags.HasFlag(NetFlags.MapAndMissions)) { - if (GameMain.GameSession.OwnedSubmarines.Any(s => s.Name == subList[i].Name)) + msg.Write(GetLastUpdateIdForFlag(NetFlags.MapAndMissions)); + msg.Write(ForceMapUI); + msg.Write(map.AllowDebugTeleport); + msg.Write(map.CurrentLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.CurrentLocationIndex); + msg.Write(map.SelectedLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.SelectedLocationIndex); + + if (map.CurrentLocation != null) { - ownedSubmarineIndices.Add(i); - } - } - msg.Write((ushort)ownedSubmarineIndices.Count); - foreach (int index in ownedSubmarineIndices) - { - msg.Write((ushort)index); - } - - msg.Write(map.AllowDebugTeleport); - msg.Write(reputation != null); - if (reputation != null) { msg.Write(reputation.Value); } - - // hopefully we'll never have more than 128 factions - msg.Write((byte)Factions.Count); - foreach (Faction faction in Factions) - { - msg.Write(faction.Prefab.Identifier); - msg.Write(faction.Reputation.Value); - } - - msg.Write(ForceMapUI); - - msg.Write(PurchasedHullRepairs); - msg.Write(PurchasedItemRepairs); - msg.Write(PurchasedLostShuttles); - - if (map.CurrentLocation != null) - { - msg.Write((byte)map.CurrentLocation?.AvailableMissions.Count()); - foreach (Mission mission in map.CurrentLocation.AvailableMissions) - { - msg.Write(mission.Prefab.Identifier); - if (mission.Locations[0] == mission.Locations[1]) + msg.Write((byte)map.CurrentLocation.AvailableMissions.Count()); + foreach (Mission mission in map.CurrentLocation.AvailableMissions) { - msg.Write((byte)255); - } - else - { - Location missionDestination = mission.Locations[0] == map.CurrentLocation ? mission.Locations[1] : mission.Locations[0]; - LocationConnection connection = map.CurrentLocation.Connections.Find(c => c.OtherLocation(map.CurrentLocation) == missionDestination); - msg.Write((byte)map.CurrentLocation.Connections.IndexOf(connection)); + msg.Write(mission.Prefab.Identifier); + if (mission.Locations[0] == mission.Locations[1]) + { + msg.Write((byte)255); + } + else + { + Location missionDestination = mission.Locations[0] == map.CurrentLocation ? mission.Locations[1] : mission.Locations[0]; + LocationConnection connection = map.CurrentLocation.Connections.Find(c => c.OtherLocation(map.CurrentLocation) == missionDestination); + msg.Write((byte)map.CurrentLocation.Connections.IndexOf(connection)); + } } } - - // Store balance - bool hasStores = map.CurrentLocation.Stores != null && map.CurrentLocation.Stores.Any(); - msg.Write(hasStores); - if (hasStores) + else { - msg.Write((byte)map.CurrentLocation.Stores.Count); - foreach (var store in map.CurrentLocation.Stores.Values) + msg.Write((byte)0); + } + + var selectedMissionIndices = map.GetSelectedMissionIndices(); + msg.Write((byte)selectedMissionIndices.Count()); + foreach (int selectedMissionIndex in selectedMissionIndices) + { + msg.Write((byte)selectedMissionIndex); + } + + WriteStores(msg); + } + + if (requiredFlags.HasFlag(NetFlags.SubList)) + { + msg.Write(GetLastUpdateIdForFlag(NetFlags.SubList)); + var subList = GameMain.NetLobbyScreen.GetSubList(); + List ownedSubmarineIndices = new List(); + for (int i = 0; i < subList.Count; i++) + { + if (GameMain.GameSession.OwnedSubmarines.Any(s => s.Name == subList[i].Name)) { - msg.Write(store.Identifier); - msg.Write((UInt16)store.Balance); + ownedSubmarineIndices.Add(i); } } + msg.Write((ushort)ownedSubmarineIndices.Count); + foreach (int index in ownedSubmarineIndices) + { + msg.Write((ushort)index); + } } - else + if (requiredFlags.HasFlag(NetFlags.UpgradeManager)) { - msg.Write((byte)0); - // Store balance - msg.Write(false); + msg.Write(GetLastUpdateIdForFlag(NetFlags.UpgradeManager)); + msg.Write((ushort)UpgradeManager.PendingUpgrades.Count); + foreach (var (prefab, category, level) in UpgradeManager.PendingUpgrades) + { + msg.Write(prefab.Identifier); + msg.Write(category.Identifier); + msg.Write((byte)level); + } + msg.Write((ushort)UpgradeManager.PurchasedItemSwaps.Count); + foreach (var itemSwap in UpgradeManager.PurchasedItemSwaps) + { + msg.Write(itemSwap.ItemToRemove.ID); + msg.Write(itemSwap.ItemToInstall?.Identifier ?? Identifier.Empty); + } } - WriteItems(msg, CargoManager.ItemsInBuyCrate); - WriteItems(msg, CargoManager.ItemsInSellFromSubCrate); - WriteItems(msg, CargoManager.PurchasedItems); - WriteItems(msg, CargoManager.SoldItems); - - msg.Write((ushort)UpgradeManager.PendingUpgrades.Count); - foreach (var (prefab, category, level) in UpgradeManager.PendingUpgrades) + if (requiredFlags.HasFlag(NetFlags.ItemsInBuyCrate)) { - msg.Write(prefab.Identifier); - msg.Write(category.Identifier); - msg.Write((byte)level); + msg.Write(GetLastUpdateIdForFlag(NetFlags.ItemsInBuyCrate)); + WriteItems(msg, CargoManager.ItemsInBuyCrate); + WriteStores(msg); } - msg.Write((ushort)UpgradeManager.PurchasedItemSwaps.Count); - foreach (var itemSwap in UpgradeManager.PurchasedItemSwaps) + if (requiredFlags.HasFlag(NetFlags.ItemsInSellFromSubCrate)) { - msg.Write(itemSwap.ItemToRemove.ID); - msg.Write(itemSwap.ItemToInstall?.Identifier ?? Identifier.Empty); + msg.Write(GetLastUpdateIdForFlag(NetFlags.ItemsInSellFromSubCrate)); + WriteItems(msg, CargoManager.ItemsInSellFromSubCrate); + WriteStores(msg); } - var characterData = GetClientCharacterData(c); - if (characterData?.CharacterInfo == null) + if (requiredFlags.HasFlag(NetFlags.PurchasedItems)) { - msg.Write(false); + msg.Write(GetLastUpdateIdForFlag(NetFlags.PurchasedItems)); + WriteItems(msg, CargoManager.PurchasedItems); + WriteStores(msg); + } - else + if (requiredFlags.HasFlag(NetFlags.SoldItems)) { - msg.Write(true); - characterData.CharacterInfo.ServerWrite(msg); + msg.Write(GetLastUpdateIdForFlag(NetFlags.SoldItems)); + WriteItems(msg, CargoManager.SoldItems); + WriteStores(msg); + } + if (requiredFlags.HasFlag(NetFlags.Reputation)) + { + msg.Write(GetLastUpdateIdForFlag(NetFlags.Reputation)); + Reputation reputation = Map?.CurrentLocation?.Reputation; + msg.Write(reputation != null); + if (reputation != null) { msg.Write(reputation.Value); } + + // hopefully we'll never have more than 128 factions + msg.Write((byte)Factions.Count); + foreach (Faction faction in Factions) + { + msg.Write(faction.Prefab.Identifier); + msg.Write(faction.Reputation.Value); + } + } + if (requiredFlags.HasFlag(NetFlags.CharacterInfo)) + { + msg.Write(GetLastUpdateIdForFlag(NetFlags.CharacterInfo)); + var characterData = GetClientCharacterData(c); + if (characterData?.CharacterInfo == null) + { + msg.Write(false); + } + else + { + msg.Write(true); + characterData.CharacterInfo.ServerWrite(msg); + } + } + + void WriteStores(IWriteMessage msg) + { + if (map.CurrentLocation != null) + { + // Store balance + bool hasStores = map.CurrentLocation.Stores != null && map.CurrentLocation.Stores.Any(); + msg.Write(hasStores); + if (hasStores) + { + msg.Write((byte)map.CurrentLocation.Stores.Count); + foreach (var store in map.CurrentLocation.Stores.Values) + { + msg.Write(store.Identifier); + msg.Write((UInt16)store.Balance); + } + } + } + else + { + msg.Write((byte)0); + // Store balance + msg.Write(false); + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs index d7223e2c8..4145c0b35 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs @@ -102,6 +102,7 @@ namespace Barotrauma.Items.Components { msg.Write(autoPilot); msg.Write(TryExtractEventData(extraData, out var eventData) && eventData.DockingButtonClicked); + msg.Write(user?.ID ?? Entity.NullEntityID); if (!autoPilot) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index ea54be031..ed9a1efa2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -21,7 +21,7 @@ namespace Barotrauma.Networking public UInt16 LastSentEntityEventID = 0; public UInt16 LastRecvEntityEventID = 0; - public UInt16 LastRecvCampaignUpdate = 0; + public readonly Dictionary LastRecvCampaignUpdate = new Dictionary(); public UInt16 LastRecvCampaignSave = 0; public (UInt16 saveId, float time) LastCampaignSaveSendTime; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs index ade75cd83..54bbd573c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs @@ -1,5 +1,5 @@ using System; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Threading.Tasks; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 8b3b20d36..234e94be4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -155,7 +155,7 @@ namespace Barotrauma.Networking else { Log("Using SteamP2P networking.", ServerLog.MessageType.ServerMessage); - serverPeer = new SteamP2PServerPeer(ownerSteamId.Value, serverSettings); + serverPeer = new SteamP2PServerPeer(ownerSteamId.Value, ownerKey.Value, serverSettings); } serverPeer.OnInitializationComplete = OnInitializationComplete; @@ -746,7 +746,7 @@ namespace Barotrauma.Networking string seed = inc.ReadString(); string subName = inc.ReadString(); string subHash = inc.ReadString(); - CampaignSettings settings = new CampaignSettings(inc); + CampaignSettings settings = INetSerializableStruct.Read(inc); var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash); @@ -767,8 +767,7 @@ namespace Barotrauma.Networking string localSavePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveName); if (connectedClient.HasPermission(ClientPermissions.SelectMode) || connectedClient.HasPermission(ClientPermissions.ManageCampaign)) { - ServerSettings.RadiationEnabled = settings.RadiationEnabled; - ServerSettings.MaxMissionCount = settings.MaxMissionCount; + ServerSettings.CampaignSettings = settings; ServerSettings.SaveSettings(); MultiPlayerCampaign.StartNewCampaign(localSavePath, matchingSub.FilePath, seed, settings); } @@ -833,6 +832,9 @@ namespace Barotrauma.Networking case ClientPacketHeader.EVENTMANAGER_RESPONSE: GameMain.GameSession?.EventManager.ServerRead(inc, connectedClient); break; + case ClientPacketHeader.UPDATE_CHARACTERINFO: + UpdateCharacterInfo(inc, connectedClient); + break; case ClientPacketHeader.ERROR: HandleClientError(inc, connectedClient); break; @@ -1050,9 +1052,11 @@ namespace Barotrauma.Networking if (c.LastRecvCampaignSave > 0) { byte campaignID = inc.ReadByte(); - c.LastRecvCampaignUpdate = inc.ReadUInt16(); + foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + c.LastRecvCampaignUpdate[netFlag] = inc.ReadUInt16(); + } bool characterDiscarded = inc.ReadBoolean(); - if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) { if (characterDiscarded) { campaign.DiscardClientCharacterData(c); } @@ -1061,7 +1065,11 @@ namespace Barotrauma.Networking if (campaign.CampaignID != campaignID) { c.LastRecvCampaignSave = (ushort)(campaign.LastSaveID - 1); - c.LastRecvCampaignUpdate = (ushort)(campaign.LastUpdateID - 1); + foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + c.LastRecvCampaignUpdate[netFlag] = + (UInt16)(campaign.GetLastUpdateIdForFlag(netFlag) - 1); + } } } } @@ -1122,9 +1130,11 @@ namespace Barotrauma.Networking if (c.LastRecvCampaignSave > 0) { byte campaignID = inc.ReadByte(); - c.LastRecvCampaignUpdate = inc.ReadUInt16(); + foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + c.LastRecvCampaignUpdate[netFlag] = inc.ReadUInt16(); + } bool characterDiscarded = inc.ReadBoolean(); - if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) { if (characterDiscarded) { campaign.DiscardClientCharacterData(c); } @@ -1133,7 +1143,11 @@ namespace Barotrauma.Networking if (campaign.CampaignID != campaignID) { c.LastRecvCampaignSave = (ushort)(campaign.LastSaveID - 1); - c.LastRecvCampaignUpdate = (ushort)(campaign.LastUpdateID - 1); + foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + c.LastRecvCampaignUpdate[netFlag] = + (UInt16)(campaign.GetLastUpdateIdForFlag(netFlag) - 1); + } } } } @@ -1370,7 +1384,7 @@ namespace Barotrauma.Networking if (gameStarted) { Log("Client \"" + GameServer.ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); - if (mpCampaign != null && Level.IsLoadedOutpost && save) + if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save) { mpCampaign.SavePlayers(); GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); @@ -1672,8 +1686,7 @@ namespace Barotrauma.Networking outmsg.Write(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server outmsg.Write(c.LastSentEntityEventID); - if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode && - NetIdUtils.IdMoreRecent(campaign.LastUpdateID, c.LastRecvCampaignUpdate)) + if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode) { outmsg.Write(true); outmsg.WritePadBits(); @@ -1899,8 +1912,7 @@ namespace Barotrauma.Networking int campaignBytes = outmsg.LengthBytes; var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; if (outmsg.LengthBytes < MsgConstants.MTU - 500 && - campaign != null && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode && - NetIdUtils.IdMoreRecent(campaign.LastUpdateID, c.LastRecvCampaignUpdate)) + campaign != null && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode) { outmsg.Write(true); outmsg.WritePadBits(); @@ -2049,7 +2061,10 @@ namespace Barotrauma.Networking var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; msg.Write(campaign == null ? (byte)0 : campaign.CampaignID); msg.Write(campaign == null ? (UInt16)0 : campaign.LastSaveID); - msg.Write(campaign == null ? (UInt16)0 : campaign.LastUpdateID); + foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + msg.Write(campaign == null ? (UInt16)0 : campaign.GetLastUpdateIdForFlag(flag)); + } connectedClients.ForEach(c => c.ReadyToStart = false); @@ -2077,7 +2092,7 @@ namespace Barotrauma.Networking } } - startGameCoroutine = GameMain.Instance.ShowLoading(StartGame(selectedSub, selectedShuttle, selectedMode, CampaignSettings.Unsure), false); + startGameCoroutine = GameMain.Instance.ShowLoading(StartGame(selectedSub, selectedShuttle, selectedMode, CampaignSettings.Empty), false); yield return CoroutineStatus.Success; } @@ -2195,7 +2210,7 @@ namespace Barotrauma.Networking Level.Loaded?.SpawnNPCs(); Level.Loaded?.SpawnCorpses(); Level.Loaded?.PrepareBeaconStation(); - AutoItemPlacer.SpawnItems(); + AutoItemPlacer.SpawnItems(campaign?.Settings.StartItemSet); CrewManager crewManager = campaign?.CrewManager; @@ -3203,18 +3218,27 @@ namespace Barotrauma.Networking if (checkActiveVote && Voting.ActiveVote != null) { - int yes = GameMain.Server.ConnectedClients.Count(c => c.InGame && c.GetVote(Voting.ActiveVote.VoteType) == 2); - int no = GameMain.Server.ConnectedClients.Count(c => c.InGame && c.GetVote(Voting.ActiveVote.VoteType) == 1); - int max = GameMain.Server.ConnectedClients.Count(c => c.InGame); - // Required ratio cannot be met - if (no / (float)max > 1f - serverSettings.VoteRequiredRatio) - { - Voting.ActiveVote.Finish(Voting, passed: false); - } - else if (yes / (float)max >= serverSettings.VoteRequiredRatio) + var inGameClients = GameMain.Server.ConnectedClients.Where(c => c.InGame); + if (inGameClients.Count() == 1) { Voting.ActiveVote.Finish(Voting, passed: true); - } + } + else + { + var eligibleClients = inGameClients.Where(c => c != Voting.ActiveVote.VoteStarter); + int yes = eligibleClients.Count(c => c.GetVote(Voting.ActiveVote.VoteType) == 2); + int no = eligibleClients.Count(c => c.GetVote(Voting.ActiveVote.VoteType) == 1); + int max = eligibleClients.Count(); + // Required ratio cannot be met + if (no / (float)max > 1f - serverSettings.VoteRequiredRatio) + { + Voting.ActiveVote.Finish(Voting, passed: false); + } + else if (yes / (float)max >= serverSettings.VoteRequiredRatio) + { + Voting.ActiveVote.Finish(Voting, passed: true); + } + } } Client.UpdateKickVotes(connectedClients); @@ -3295,7 +3319,7 @@ namespace Barotrauma.Networking if (voteType != VoteType.PurchaseSub) { - GameMain.GameSession.SwitchSubmarine(targetSubmarine, deliveryFee, starter); + GameMain.GameSession.SwitchSubmarine(targetSubmarine, subVote.TransferItems, deliveryFee, starter); } Voting.StopSubmarineVote(true); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs index 2d1cb634c..8718bc4d5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs @@ -16,14 +16,21 @@ namespace Barotrauma.Networking private set; } - public SteamP2PServerPeer(UInt64 steamId, ServerSettings settings) + private UInt64 ownerKey64 => unchecked((UInt64)ownerKey.Value); + + private UInt64 ReadSteamId(IReadMessage inc) + => inc.ReadUInt64() ^ ownerKey64; + private void WriteSteamId(IWriteMessage msg, UInt64 val) + => msg.Write(val ^ ownerKey64); + + public SteamP2PServerPeer(UInt64 steamId, int ownerKey, ServerSettings settings) { serverSettings = settings; connectedClients = new List(); pendingClients = new List(); - ownerKey = null; + this.ownerKey = ownerKey; OwnerSteamID = steamId; @@ -33,7 +40,7 @@ namespace Barotrauma.Networking public override void Start() { IWriteMessage outMsg = new WriteOnlyMessage(); - outMsg.Write(OwnerSteamID); + WriteSteamId(outMsg, OwnerSteamID); outMsg.Write((byte)DeliveryMethod.Reliable); outMsg.Write((byte)(PacketHeader.IsConnectionInitializationStep | PacketHeader.IsServerMessage)); @@ -122,8 +129,8 @@ namespace Barotrauma.Networking { if (!started) { return; } - UInt64 senderSteamId = inc.ReadUInt64(); - UInt64 ownerSteamId = inc.ReadUInt64(); + UInt64 senderSteamId = ReadSteamId(inc); + UInt64 ownerSteamId = ReadSteamId(inc); PacketHeader packetHeader = (PacketHeader)inc.ReadByte(); @@ -264,7 +271,7 @@ namespace Barotrauma.Networking IWriteMessage msgToSend = new WriteOnlyMessage(); byte[] msgData = new byte[16]; msg.PrepareForSending(ref msgData, compressPastThreshold, out bool isCompressed, out int length); - msgToSend.Write(conn.SteamID); + WriteSteamId(msgToSend, conn.SteamID); msgToSend.Write((byte)deliveryMethod); msgToSend.Write((byte)((isCompressed ? PacketHeader.IsCompressed : PacketHeader.None) | PacketHeader.IsServerMessage)); msgToSend.Write((UInt16)length); @@ -281,7 +288,7 @@ namespace Barotrauma.Networking if (string.IsNullOrWhiteSpace(msg)) { return; } IWriteMessage msgToSend = new WriteOnlyMessage(); - msgToSend.Write(steamId); + WriteSteamId(msgToSend, steamId); msgToSend.Write((byte)DeliveryMethod.Reliable); msgToSend.Write((byte)(PacketHeader.IsDisconnectMessage | PacketHeader.IsServerMessage)); msgToSend.Write(msg); @@ -318,7 +325,7 @@ namespace Barotrauma.Networking protected override void SendMsgInternal(NetworkConnection conn, DeliveryMethod deliveryMethod, IWriteMessage msg) { IWriteMessage msgToSend = new WriteOnlyMessage(); - msgToSend.Write(conn.SteamID); + WriteSteamId(msgToSend, conn.SteamID); msgToSend.Write((byte)deliveryMethod); msgToSend.Write(msg.Buffer, 0, msg.LengthBytes); byte[] bufToSend = (byte[])msgToSend.Buffer.Clone(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index bc841e5a9..39f7833c9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -1,11 +1,9 @@ -using Barotrauma.IO; -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Barotrauma.IO; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Xml.Linq; -using Barotrauma.Extensions; namespace Barotrauma.Networking { @@ -36,7 +34,7 @@ namespace Barotrauma.Networking => LastUpdateIdForFlag[flag] = (UInt16)(GameMain.NetLobbyScreen.LastUpdateID + 1); private bool IsFlagRequired(Client c, NetFlags flag) - => LastUpdateIdForFlag[flag] > c.LastRecvLobbyUpdate; + => NetIdUtils.IdMoreRecent(LastUpdateIdForFlag[flag], c.LastRecvLobbyUpdate); public NetFlags GetRequiredFlags(Client c) => LastUpdateIdForFlag.Keys @@ -56,7 +54,7 @@ namespace Barotrauma.Networking { var property = netProperties[key]; property.SyncValue(); - if (property.LastUpdateID > c.LastRecvLobbyUpdate) + if (NetIdUtils.IdMoreRecent(property.LastUpdateID, c.LastRecvLobbyUpdate)) { outMsg.Write(key); netProperties[key].Write(outMsg); @@ -257,7 +255,7 @@ namespace Barotrauma.Networking doc.Root.SetAttributeValue("queryport", QueryPort); #endif doc.Root.SetAttributeValue("password", password ?? ""); - + doc.Root.SetAttributeValue("enableupnp", EnableUPnP); doc.Root.SetAttributeValue("autorestart", autoRestart); @@ -266,11 +264,12 @@ namespace Barotrauma.Networking doc.Root.SetAttributeValue("ServerMessage", ServerMessageText); doc.Root.SetAttributeValue("HiddenSubs", string.Join(",", HiddenSubs)); - + doc.Root.SetAttributeValue("AllowedRandomMissionTypes", string.Join(",", AllowedRandomMissionTypes)); doc.Root.SetAttributeValue("AllowedClientNameChars", string.Join(",", AllowedClientNameChars.Select(c => $"{c.Start}-{c.End}"))); SerializableProperty.SerializeProperties(this, doc.Root, true); + doc.Root.Add(CampaignSettings.Save()); System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { @@ -399,7 +398,7 @@ namespace Barotrauma.Networking ServerName = doc.Root.GetAttributeString("name", ""); if (ServerName.Length > NetConfig.ServerNameMaxLength) { ServerName = ServerName.Substring(0, NetConfig.ServerNameMaxLength); } ServerMessageText = doc.Root.GetAttributeString("ServerMessage", ""); - + GameMain.NetLobbyScreen.SelectedModeIdentifier = GameModeIdentifier; //handle Random as the mission type, which is no longer a valid setting //MissionType.All offers equivalent functionality @@ -410,6 +409,14 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetBotCount(BotCount); MonsterEnabled ??= CharacterPrefab.Prefabs.Select(p => (p.Identifier, true)).ToDictionary(); + + foreach (XElement element in doc.Root.Elements()) + { + if (element.Name.ToIdentifier() == nameof(Barotrauma.CampaignSettings)) + { + CampaignSettings = new CampaignSettings(element); + } + } } public string SelectNonHiddenSubmarine(string current = null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index 13b9156e4..4a3577fe5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -27,11 +27,13 @@ namespace Barotrauma public VoteState State { get; set; } public SubmarineInfo Sub; + public bool TransferItems; public int DeliveryFee; - public SubmarineVote(Client starter, SubmarineInfo subInfo, int deliveryFee, VoteType voteType) + public SubmarineVote(Client starter, SubmarineInfo subInfo, bool transferItems, int deliveryFee, VoteType voteType) { Sub = subInfo; + TransferItems = transferItems; DeliveryFee = deliveryFee; VoteType = voteType; State = VoteState.Started; @@ -101,15 +103,12 @@ namespace Barotrauma private readonly Dictionary rejectedVoteTimes = new Dictionary(); - private void StartSubmarineVote(SubmarineInfo subInfo, VoteType voteType, Client sender) + private void StartSubmarineVote(SubmarineInfo subInfo, bool transferItems, VoteType voteType, Client sender) { - if (ActiveVote == null) - { - sender.SetVote(voteType, 2); - } var subVote = new SubmarineVote( sender, subInfo, + transferItems, voteType == VoteType.SwitchSub ? GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation) : 0, voteType); StartOrEnqueueVote(subVote); @@ -152,10 +151,6 @@ namespace Barotrauma { return; } - if (ActiveVote == null) - { - starter.SetVote(VoteType.TransferMoney, 2); - } StartOrEnqueueVote(new TransferVote(starter, from, transferAmount, to)); GameMain.Server.UpdateVoteStatus(checkActiveVote: false); } @@ -205,11 +200,19 @@ namespace Barotrauma if (ActiveVote.Timer >= GameMain.NetworkMember.ServerSettings.VoteTimeout) { + var inGameClients = GameMain.Server.ConnectedClients.Where(c => c.InGame); + var eligibleClients = inGameClients.Where(c => c != ActiveVote.VoteStarter); + // Do not take unanswered into account for total - int yes = GameMain.Server.ConnectedClients.Count(c => c.InGame && c.GetVote(ActiveVote.VoteType) == 2); - int no = GameMain.Server.ConnectedClients.Count(c => c.InGame && c.GetVote(ActiveVote.VoteType) == 1); + int yes = eligibleClients.Count(c => c.GetVote(ActiveVote.VoteType) == 2); + int no = eligibleClients.Count(c => c.GetVote(ActiveVote.VoteType) == 1); int total = Math.Max(yes + no, 1); - ActiveVote.Finish(this, passed: yes / (float)(total) >= GameMain.NetworkMember.ServerSettings.VoteRequiredRatio); + + bool passed = + yes / (float)total >= GameMain.NetworkMember.ServerSettings.VoteRequiredRatio || + inGameClients.Count() == 1; + + ActiveVote.Finish(this, passed); } } @@ -293,12 +296,13 @@ namespace Barotrauma { string subName = inc.ReadString(); SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName); + bool transferItems = inc.ReadBoolean(); if (!ShouldRejectVote(sender, voteType)) { if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign && (campaign.CanPurchaseSub(subInfo, sender) || GameMain.GameSession.IsSubmarineOwned(subInfo))) { - StartSubmarineVote(subInfo, voteType, sender); + StartSubmarineVote(subInfo, transferItems, voteType, sender); } } } @@ -355,22 +359,24 @@ namespace Barotrauma { msg.Write((byte)ActiveVote.VoteType); if (ActiveVote.State != VoteState.None && ActiveVote.VoteType != VoteType.Unknown) - { - var yesClients = GameMain.Server.ConnectedClients.FindAll(c => c.InGame && c.GetVote(ActiveVote.VoteType) == 2); - msg.Write((byte)yesClients.Count); + { + var eligibleClients = GameMain.Server.ConnectedClients.Where(c => c.InGame && c != ActiveVote.VoteStarter); + + var yesClients = eligibleClients.Where(c => c.GetVote(ActiveVote.VoteType) == 2); + msg.Write((byte)yesClients.Count()); foreach (Client c in yesClients) { msg.Write(c.ID); } - var noClients = GameMain.Server.ConnectedClients.FindAll(c => c.InGame && c.GetVote(ActiveVote.VoteType) == 1); - msg.Write((byte)noClients.Count); + var noClients = eligibleClients.Where(c => c.GetVote(ActiveVote.VoteType) == 1); + msg.Write((byte)noClients.Count()); foreach (Client c in noClients) { msg.Write(c.ID); } - msg.Write((byte)GameMain.Server.ConnectedClients.Count(c => c.InGame)); + msg.Write((byte)eligibleClients.Count()); switch (ActiveVote.State) { @@ -384,6 +390,7 @@ namespace Barotrauma case VoteType.PurchaseAndSwitchSub: case VoteType.SwitchSub: msg.Write((ActiveVote as SubmarineVote).Sub.Name); + msg.Write((ActiveVote as SubmarineVote).TransferItems); break; case VoteType.TransferMoney: var transferVote = (ActiveVote as TransferVote); @@ -405,8 +412,10 @@ namespace Barotrauma case VoteType.PurchaseSub: case VoteType.PurchaseAndSwitchSub: case VoteType.SwitchSub: - msg.Write((ActiveVote as SubmarineVote).Sub.Name); - msg.Write((short)(ActiveVote as SubmarineVote).DeliveryFee); + var subVote = ActiveVote as SubmarineVote; + msg.Write(subVote.Sub.Name); + msg.Write(subVote.TransferItems); + msg.Write((short)subVote.DeliveryFee); break; } break; diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index a1886f6b2..613378097 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,12 +6,13 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.2.0 + 0.18.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable + true ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml new file mode 100644 index 000000000..a80fcbe81 --- /dev/null +++ b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs index f13ea197e..82b3e7591 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs @@ -34,14 +34,18 @@ namespace Barotrauma public float SoundRange { get { return soundRange; } - set + set { if (float.IsNaN(value)) { DebugConsole.ThrowError("Attempted to set the SoundRange of an AITarget to NaN.\n" + Environment.StackTrace.CleanupStackTrace()); return; } - soundRange = MathHelper.Clamp(value, MinSoundRange, MaxSoundRange); + soundRange = MathHelper.Clamp(value, MinSoundRange, MaxSoundRange); + if (soundRange > 0.0f && !Static && FadeOutTime > 0.0f) + { + NeedsUpdate = true; + } } } @@ -55,7 +59,11 @@ namespace Barotrauma DebugConsole.ThrowError("Attempted to set the SightRange of an AITarget to NaN.\n" + Environment.StackTrace.CleanupStackTrace()); return; } - sightRange = MathHelper.Clamp(value, MinSightRange, MaxSightRange); + sightRange = MathHelper.Clamp(value, MinSightRange, MaxSightRange); + if (sightRange > 0 && !Static && FadeOutTime > 0.0f) + { + NeedsUpdate = true; + } } } @@ -99,13 +107,33 @@ namespace Barotrauma /// public bool InDetectable { - get => inDetectable || (SoundRange <= 0 && SightRange <= 0); - set => inDetectable = value; + get + { + return inDetectable || (SoundRange <= 0 && SightRange <= 0); + } + set + { + inDetectable = value; + if (inDetectable) + { + NeedsUpdate = true; + } + } } + public float MinSoundRange, MinSightRange; public float MaxSoundRange = 100000, MaxSightRange = 100000; + /// + /// Does the AI target do something that requires Update() to be called (e.g. static targets don't need to be updated) + /// + public bool NeedsUpdate + { + get; + private set; + } = true; + public TargetType Type { get; private set; } public enum TargetType @@ -190,14 +218,22 @@ namespace Barotrauma if (!Static && FadeOutTime > 0) { // The aitarget goes silent/invisible if the components don't keep it active - if (!StaticSight && SightRange > 0) + if (!StaticSight && sightRange > 0) { DecreaseSightRange(deltaTime); } - if (!StaticSound && SoundRange > 0) + if (!StaticSound && soundRange > 0) { DecreaseSoundRange(deltaTime); } + if (sightRange <= 0 && soundRange <= 0) + { + NeedsUpdate = false; + } + } + else + { + NeedsUpdate = false; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 779a6fb30..8897ebecb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -1440,14 +1440,33 @@ namespace Barotrauma } else if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && distance < AttackLimb.attack.Range * 5) { - reachTimer += deltaTime; - if (reachTimer > reachTimeOut) + Vector2 targetVelocity = Vector2.Zero; + Submarine targetSub = SelectedAiTarget.Entity.Submarine; + if (targetSub != null) { - reachTimer = 0; - IgnoreTarget(SelectedAiTarget); - State = AIState.Idle; - ResetAITarget(); - return; + targetVelocity = targetSub.Velocity; + } + else if (targetCharacter != null) + { + targetVelocity = targetCharacter.AnimController.Collider.LinearVelocity; + } + else if (SelectedAiTarget.Entity is Item i && i.body != null) + { + targetVelocity = i.body.LinearVelocity; + } + float mySpeed = Character.AnimController.Collider.LinearVelocity.LengthSquared(); + float targetSpeed = targetVelocity.LengthSquared(); + if (mySpeed < 0.1f || mySpeed > targetSpeed) + { + reachTimer += deltaTime; + if (reachTimer > reachTimeOut) + { + reachTimer = 0; + IgnoreTarget(SelectedAiTarget); + State = AIState.Idle; + ResetAITarget(); + return; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index a63a5e0ad..ff7c37218 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -866,8 +866,8 @@ namespace Barotrauma var container = i.GetComponent(); if (container == null) { return 0; } if (!container.Inventory.CanBePut(containableItem)) { return 0; } - var rootContainer = container.Item.GetRootContainer(); - if (rootContainer?.GetComponent() != null || rootContainer?.GetComponent() != null) { return 0; } + var rootContainer = container.Item.GetRootContainer() ?? container.Item; + if (rootContainer.GetComponent() != null || rootContainer.GetComponent() != null) { return 0; } if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined)) { if (isRestrictionsDefined) @@ -882,7 +882,12 @@ namespace Barotrauma } else { - return isPreferencesDefined ? 0 : 1; + if (isPreferencesDefined) + { + // Use any valid locker as a fall back container. + return container.Item.HasTag("locker") ? 0.5f : 0; + } + return 1; } } } @@ -1950,11 +1955,10 @@ namespace Barotrauma enemyFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(enemyCount * 0.9f, 0, 1)); } float dangerousItemsFactor = 1f; - foreach (Item item in Item.ItemList) + foreach (Item item in Item.DangerousItems) { - if (item.CurrentHull != hull) { continue; } - if (item.Prefab != null && item.Prefab.IsDangerous) - { + if (item.CurrentHull == hull) + { dangerousItemsFactor = 0; break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index 5e618f9e7..1a14bd206 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -245,7 +245,7 @@ namespace Barotrauma { get { - if (IgnoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) + if (IgnoreAtOutpost && Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) { if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 8807abfd1..a2567e72b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -48,16 +48,29 @@ namespace Barotrauma } else { - float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X); - float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y); - // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally). - // If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby. - float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f)); - float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100; float reduction = isPriority ? 1 : 2; - float max = AIObjectiveManager.LowestOrderPriority - reduction; - float devotion = CumulatedDevotion / 100; - Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); + float maxPriority = AIObjectiveManager.LowestOrderPriority - reduction; + if (operateObjective != null && objectiveManager.GetActiveObjective() is AIObjectiveFixLeaks fixLeaks && fixLeaks.CurrentSubObjective == this) + { + // Prioritize leaks that we are already fixing + Priority = maxPriority; + } + else + { + float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X); + float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y); + // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally). + // If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby. + float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f)); + if (Leak.linkedTo.Any(e => e is Hull h && h == character.CurrentHull)) + { + // Double the distance when the leak can be accessed from the current hull. + distanceFactor *= 2; + } + float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100; + float devotion = CumulatedDevotion / 100; + Priority = MathHelper.Lerp(0, maxPriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); + } } return Priority; } @@ -202,7 +215,7 @@ namespace Barotrauma // This is an approximation, because we don't know the exact reach until the pose is taken. // And even then the actual range depends on the direction we are aiming to. // Found out that without any multiplier the value (209) is often too short. - return repairTool.Range + armLength * 1.3f; + return repairTool.Range + armLength * 2; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 762e8cd08..a63159e19 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -185,6 +185,11 @@ namespace Barotrauma { PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null); } + else + { + PathSteering.ResetPath(); + PathSteering.Reset(); + } } else { @@ -290,12 +295,25 @@ namespace Barotrauma { PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null); } + else + { + PathSteering.ResetPath(); + PathSteering.Reset(); + } } } public void Wander(float deltaTime) { - if (character.IsClimbing) { return; } + if (character.IsClimbing) + { + if (character.AnimController.GetHeightFromFloor() < 0.1f) + { + character.AnimController.Anim = AnimController.Animation.None; + character.SelectedConstruction = null; + } + return; + } var currentHull = character.CurrentHull; if (!character.AnimController.InWater && currentHull != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 9b05c7e0a..652ce82a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -142,7 +142,7 @@ namespace Barotrauma } var order = new Order(orderPrefab, autonomousObjective.Option, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character); if (order == null) { continue; } - if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) + if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) && Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) { if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index 27b75c722..ee2c985fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -238,7 +238,7 @@ namespace Barotrauma }; if (repairTool != null) { - objective.CloseEnough = repairTool.Range * 0.75f; + objective.CloseEnough = AIObjectiveFixLeak.CalculateReach(repairTool, character); } return objective; }, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs index d9039091d..e4e900990 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs @@ -1,8 +1,6 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; -using FarseerPhysics; -using Barotrauma.Extensions; namespace Barotrauma { @@ -90,6 +88,10 @@ namespace Barotrauma { steering = Vector2.Normalize(steering) * Math.Abs(speed); } + if (host is AIController aiController && aiController?.Character.CharacterHealth.GetAfflictionOfType("invertcontrols".ToIdentifier()) != null) + { + steering = -steering; + } host.Steering = steering; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 81b328b03..f0818dd66 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -22,8 +22,7 @@ namespace Barotrauma { if (_ragdollParams == null) { - #warning TODO: this is kinda janky, this should probably be done better - _ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.VariantOf.IfEmpty(character.SpeciesName)); + _ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.SpeciesName); if (!character.VariantOf.IsEmpty) { _ragdollParams.ApplyVariantScale(character.Params.VariantFile); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 294285a87..b760a2b2a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -74,7 +74,7 @@ namespace Barotrauma } } - public bool HasMultipleLimbsOfSameType => limbs == null ? false : Limbs.Length > limbDictionary.Count; + public bool HasMultipleLimbsOfSameType => limbs != null && limbs.Length > limbDictionary.Count; private bool frozen; public bool Frozen @@ -1850,36 +1850,30 @@ namespace Barotrauma } /// - /// Note that if there are multiple limbs of the same type, only the first of them is found in the dictionary. + /// Note that if there are multiple limbs of the same type, only the first (valid) limb is returned. /// public Limb GetLimb(LimbType limbType, bool excludeSevered = true) { - Limb limb = null; - if (HasMultipleLimbsOfSameType) + if (limbDictionary.TryGetValue(limbType, out Limb limb)) { - for (int i = 0; i < 10; i++) + if (excludeSevered && limb.IsSevered) { - limbDictionary.TryGetValue(limbType, out limb); - if (limb == null) + limb = null; + } + } + if (limb == null && HasMultipleLimbsOfSameType) + { + // Didn't find a (valid) limb of the matching type. If there's multiple limbs of the same type, check the other limbs. + foreach (var l in limbs) + { + if (l.type != limbType) { continue; } + if (!excludeSevered || !l.IsSevered) { - // No limbs found - break; - } - if (!excludeSevered || !limb.IsSevered) - { - // Found a valid limb + limb = l; break; } } } - else - { - limbDictionary.TryGetValue(limbType, out limb); - } - if (excludeSevered && limb != null && limb.IsSevered) - { - limb = null; - } return limb; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index d31d943a0..e1d53f45d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -117,7 +117,29 @@ namespace Barotrauma protected Key[] keys; - public HumanPrefab HumanPrefab; + private HumanPrefab humanPrefab; + public HumanPrefab HumanPrefab + { + get { return humanPrefab; } + set + { + if (humanPrefab == value) { return; } + humanPrefab = value; + + if (humanPrefab != null) + { + HumanPrefabHealthMultiplier = humanPrefab.HealthMultiplier; + if (GameMain.NetworkMember != null) + { + HumanPrefabHealthMultiplier *= humanPrefab.HealthMultiplierInMultiplayer; + } + } + else + { + HumanPrefabHealthMultiplier = 1.0f; + } + } + } private CharacterTeamType teamID; public CharacterTeamType TeamID @@ -1192,7 +1214,7 @@ namespace Barotrauma CharacterHealth = new CharacterHealth(selectedHealthElement, this, limbHealthElement); } - if (Params.Husk && speciesName != "husk") + if (Params.Husk && speciesName != "husk" && Prefab.VariantOf != "husk") { // Get the non husked name and find the ragdoll with it var matchingAffliction = AfflictionPrefab.List @@ -1392,7 +1414,7 @@ namespace Barotrauma if (inputType == InputType.Up || inputType == InputType.Down || inputType == InputType.Left || inputType == InputType.Right) { - var invertControls = CharacterHealth.GetAffliction("invertcontrols"); + var invertControls = CharacterHealth.GetAfflictionOfType("invertcontrols".ToIdentifier()); if (invertControls != null) { switch (inputType) @@ -1652,14 +1674,9 @@ namespace Barotrauma } /// - /// Can be used to modify a character's health for runtime session. Change with AddHealthMultiplier + /// Health multiplier of the human prefab this character is an instance of (if any) /// - public float StaticHealthMultiplier { get; private set; } = 1; - - public void AddStaticHealthMultiplier(float newMultiplier) - { - StaticHealthMultiplier *= newMultiplier; - } + public float HumanPrefabHealthMultiplier { get; private set; } = 1; /// /// Speed reduction from the current limb specific damage. Min 0, max 1. @@ -4824,21 +4841,21 @@ namespace Barotrauma } } - private readonly List abilityFlags = new List(); + private AbilityFlags abilityFlags; public void AddAbilityFlag(AbilityFlags abilityFlag) { - abilityFlags.Add(abilityFlag); + abilityFlags |= abilityFlag; } public void RemoveAbilityFlag(AbilityFlags abilityFlag) { - abilityFlags.Remove(abilityFlag); + abilityFlags &= ~abilityFlag; } public bool HasAbilityFlag(AbilityFlags abilityFlag) { - return abilityFlags.Contains(abilityFlag) || CharacterHealth.HasFlag(abilityFlag); + return abilityFlags.HasFlag(abilityFlag) || CharacterHealth.HasFlag(abilityFlag); } private readonly Dictionary abilityResistances = new Dictionary(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 93fe641e6..c8dca9919 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -35,6 +35,7 @@ namespace Barotrauma if (newValue > _strength) { PendingAdditionStrength = Prefab.GrainBurst; + Duration = Prefab.Duration; } _strength = newValue; } @@ -60,6 +61,8 @@ namespace Barotrauma public double AppliedAsSuccessfulTreatmentTime, AppliedAsFailedTreatmentTime; + public float Duration; + /// /// Which character gave this affliction /// @@ -75,6 +78,8 @@ namespace Barotrauma _strength = strength; Identifier = prefab.Identifier; + Duration = prefab.Duration; + foreach (var periodicEffect in prefab.PeriodicEffects) { PeriodicEffectTimers[periodicEffect] = Rand.Range(periodicEffect.MinInterval, periodicEffect.MaxInterval); @@ -315,8 +320,7 @@ namespace Barotrauma public bool HasFlag(AbilityFlags flagType) { if (!(GetViableEffect() is AfflictionPrefab.Effect currentEffect)) { return false; } - - return currentEffect.AfflictionAbilityFlags.Contains(flagType); + return currentEffect.AfflictionAbilityFlags.HasFlag(flagType); } private AfflictionPrefab.Effect GetViableEffect() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 4e277a25a..04d620a18 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -235,7 +235,7 @@ namespace Barotrauma public Identifier[] BlockTransformation { get; private set; } public readonly Dictionary AfflictionStatValues = new Dictionary(); - public readonly HashSet AfflictionAbilityFlags = new HashSet(); + public AbilityFlags AfflictionAbilityFlags; //statuseffects applied on the character when the affliction is active public readonly List StatusEffects = new List(); @@ -265,7 +265,7 @@ namespace Barotrauma break; case "abilityflag": var flagType = CharacterAbilityGroup.ParseFlagType(subElement.GetAttributeString("flagtype", ""), parentDebugName); - AfflictionAbilityFlags.Add(flagType); + AfflictionAbilityFlags |= flagType; break; case "affliction": DebugConsole.AddWarning($"Error in affliction \"{parentDebugName}\" - additional afflictions caused by the affliction should be configured inside status effects."); @@ -354,6 +354,11 @@ namespace Barotrauma //how strong the affliction needs to be before bots attempt to treat it public readonly float TreatmentThreshold = 5.0f; + /// + /// The affliction is automatically removed after this time. 0 = unlimited + /// + public readonly float Duration; + //how much karma changes when a player applies this affliction to someone (per strength of the affliction) public float KarmaChangeOnApplied; @@ -407,8 +412,10 @@ namespace Barotrauma !IsBuff && AfflictionType != "geneticmaterialbuff" && AfflictionType != "geneticmaterialdebuff"); - HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier).ToLowerInvariant(), 1f); - BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost).ToLowerInvariant(), 0); + HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier), 1f); + BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost), 0); + + Duration = element.GetAttributeFloat(nameof(Duration), 0.0f); if (element.GetAttribute("nameidentifier") != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 5e4435b7d..bdd89998c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -1,12 +1,11 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Xml.Linq; -using Barotrauma.Networking; -using Barotrauma.Extensions; -using System.Globalization; -using Barotrauma.Abilities; namespace Barotrauma { @@ -148,7 +147,7 @@ namespace Barotrauma { max += Character.Info.Job.Prefab.VitalityModifier; } - max *= Character.StaticHealthMultiplier; + max *= Character.HumanPrefabHealthMultiplier; max *= 1f + Character.GetStatValue(StatTypes.MaximumHealthMultiplier); return max * Character.HealthMultiplier; } @@ -700,6 +699,7 @@ namespace Barotrauma newStrength = Math.Min(existingAffliction.Prefab.MaxStrength, newStrength); if (existingAffliction == stunAffliction) { Character.SetStun(newStrength, true, true); } existingAffliction.Strength = newStrength; + existingAffliction.Duration = existingAffliction.Prefab.Duration; if (newAffliction.Source != null) { existingAffliction.Source = newAffliction.Source; } CalculateVitality(); if (Vitality <= MinVitality) @@ -759,6 +759,15 @@ namespace Barotrauma if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); } continue; } + if (affliction.Prefab.Duration > 0.0f) + { + affliction.Duration -= deltaTime; + if (affliction.Duration <= 0.0f) + { + afflictionsToRemove.Add(affliction); + continue; + } + } afflictionsToUpdate.Add(kvp); } foreach (KeyValuePair kvp in afflictionsToUpdate) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index c2d64b348..7e8a539ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -112,12 +112,6 @@ namespace Barotrauma public void InitializeCharacter(Character npc, ISpatialEntity positionToStayIn = null) { - npc.AddStaticHealthMultiplier(HealthMultiplier); - if (GameMain.NetworkMember != null) - { - npc.AddStaticHealthMultiplier(HealthMultiplierInMultiplayer); - } - var humanAI = npc.AIController as HumanAIController; if (humanAI != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index 608d7f16f..4638671f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -138,61 +138,53 @@ namespace Barotrauma ragdolls = new Dictionary(); allRagdolls.Add(speciesName, ragdolls); } - if (!string.IsNullOrEmpty(fileName) && ragdolls.TryGetValue(fileName, out RagdollParams ragdoll)) { return (T)ragdoll; } - string selectedFile = null; - - void tryFolderForSpecies(Identifier species, out string err) + Identifier ragdollSpecies = speciesName; + if (CharacterPrefab.Prefabs.TryGet(speciesName, out var prefab)) { - err = null; - string folder = GetFolder(species); + if (!prefab.VariantOf.IsEmpty) + { + ragdollSpecies = prefab.VariantOf; + } + string error = null; + string folder = GetFolder(ragdollSpecies); if (!Directory.Exists(folder)) { - err = $"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll."; - selectedFile = GetDefaultFile(species); - return; - } - - string[] files = Directory.GetFiles(folder); - if (files.None()) - { - err = $"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll."; - selectedFile = GetDefaultFile(species); - } - else if (string.IsNullOrEmpty(fileName)) - { - // Files found, but none specified - selectedFile = GetDefaultFile(species); + error = $"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll."; + selectedFile = GetDefaultFile(ragdollSpecies); } else { - selectedFile = files.FirstOrDefault(f => IO.Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase)); - if (selectedFile == null) + string[] files = Directory.GetFiles(folder); + if (files.None()) { - err = $"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll."; - selectedFile = GetDefaultFile(species); + 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); + } + 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."; + selectedFile = GetDefaultFile(ragdollSpecies); + } } } + if (error != null) + { + DebugConsole.ThrowError(error); + } } - - tryFolderForSpecies(speciesName, out var error); - Identifier parentSpeciesName = CharacterPrefab.Prefabs.TryGet(speciesName, out var prefab) - ? prefab.VariantOf - : Identifier.Empty; - if (!error.IsNullOrEmpty() && !parentSpeciesName.IsEmpty) - { - tryFolderForSpecies(parentSpeciesName, out error); - } - - if (!error.IsNullOrEmpty()) - { - DebugConsole.ThrowError(error); - } - if (selectedFile == null) { throw new Exception("[RagdollParams] Selected file null!"); @@ -200,7 +192,7 @@ namespace Barotrauma DebugConsole.Log($"[RagdollParams] Loading ragdoll from {selectedFile}."); var characterPrefab = CharacterPrefab.Prefabs[speciesName]; T r = new T(); - if (r.Load(ContentPath.FromRaw(characterPrefab.ContentPackage, selectedFile), speciesName)) + if (r.Load(ContentPath.FromRaw(characterPrefab.ContentPackage, selectedFile), ragdollSpecies)) { if (!ragdolls.ContainsKey(r.Name)) { 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 7065cb683..426156bec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs @@ -1,5 +1,4 @@ using System; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs index a1e03fcb6..782856d1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs @@ -1,6 +1,4 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { abstract class AbilityConditionDataless : AbilityCondition { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs index 4c29a5b61..044d960a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs index 70ec6e1ae..5711c0ed5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs index 3f9090376..70b871963 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs @@ -1,7 +1,4 @@ using Microsoft.Xna.Framework; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Abilities { @@ -19,10 +16,9 @@ namespace Barotrauma.Abilities protected override void ApplyEffect() { - IEnumerable chosenCharacters = Character.GetFriendlyCrew(Character).Where(c => allowSelf || c != Character); - - foreach (Character character in chosenCharacters) + foreach (Character character in Character.GetFriendlyCrew(Character)) { + if (!allowSelf && character == Character) { continue; } if (maxDistance < float.MaxValue) { if (Vector2.DistanceSquared(character.WorldPosition, Character.WorldPosition) > maxDistance * maxDistance) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs index d6fc8b329..332c92e20 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityApplyStatusEffectsToAttacker : CharacterAbilityApplyStatusEffects { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToLastOrderedCharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToLastOrderedCharacter.cs index 4594c5e1e..693924271 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToLastOrderedCharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToLastOrderedCharacter.cs @@ -1,6 +1,4 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityApplyStatusEffectsToLastOrderedCharacter : CharacterAbilityApplyStatusEffects { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs index f8329aae2..94f7dfe02 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs @@ -1,6 +1,5 @@ using Microsoft.Xna.Framework; using System; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs index 1f3795dea..656e5d751 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs index a12e2ce1e..b77f4332d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityGroupEffect : CharacterAbilityGroup { @@ -30,7 +24,14 @@ namespace Barotrauma.Abilities private bool IsApplicable(AbilityObject abilityObject) { if (timesTriggered >= maxTriggerCount) { return false; } - return abilityConditions.All(c => c.MatchesCondition(abilityObject)); + foreach (var abilityCondition in abilityConditions) + { + if (!abilityCondition.MatchesCondition(abilityObject)) + { + return false; + } + } + return true; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs index 8682a47df..7cc1e24eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityGroupInterval : CharacterAbilityGroup { @@ -49,7 +43,14 @@ namespace Barotrauma.Abilities private bool IsApplicable() { if (timesTriggered >= maxTriggerCount) { return false; } - return abilityConditions.All(c => c.MatchesCondition()); + foreach (var abilityCondition in abilityConditions) + { + if (!abilityCondition.MatchesCondition()) + { + return false; + } + } + return true; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs index bfa5b6869..0cf4b3420 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs @@ -1,8 +1,6 @@ -using System; +using Barotrauma.Abilities; +using System; using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; -using Barotrauma.Abilities; namespace Barotrauma { diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs index 4f853ba01..736f5053e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs @@ -2,12 +2,10 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Globalization; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Reflection; using System.Xml.Linq; -using Barotrauma.Extensions; namespace Barotrauma { @@ -69,10 +67,10 @@ namespace Barotrauma .ToImmutableHashSet(); } - public static Result CreateFromXElement(ContentPackage contentPackage, XElement element) + public static Result CreateFromXElement(ContentPackage contentPackage, XElement element) { - static Result fail(string error, string? stackTrace = null) - => Result.Failure(error, stackTrace); + static Result fail(string error, Exception? exception = null) + => Result.Failure(new LoadError(error, exception)); Identifier elemName = element.NameAsIdentifier(); var type = Types.FirstOrDefault(t => t.Names.Contains(elemName)); @@ -95,11 +93,11 @@ namespace Barotrauma var file = type.CreateInstance(contentPackage, filePath); return file is null ? throw new Exception($"Content type is not implemented correctly") - : Result.Success(file); + : Result.Success(file); } catch (Exception e) { - return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": {e.Message}", e.StackTrace.CleanupStackTrace()); + return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": {e.Message}", e); } } @@ -125,5 +123,23 @@ namespace Barotrauma } public bool NotSyncedInMultiplayer => Types.Any(t => t.Type == GetType() && t.NotSyncedInMultiplayer); + + public readonly struct LoadError + { + public readonly string Message; + public readonly Exception? Exception; + + public LoadError(string message, Exception? exception) + { + Message = message; + Exception = exception; + } + + public override string ToString() + => Message + + (Exception is { StackTrace: var stackTrace } + ? '\n' + stackTrace.CleanupStackTrace() + : string.Empty); + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index da7f6be5a..bcb068006 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -14,7 +14,7 @@ namespace Barotrauma { public abstract class ContentPackage { - public static readonly Version MinimumHashCompatibleVersion = new Version(0, 17, 16, 0); + public static readonly Version MinimumHashCompatibleVersion = new Version(0, 18, 3, 0); public const string LocalModsDir = "LocalMods"; public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine( @@ -33,11 +33,11 @@ namespace Barotrauma public readonly Version GameVersion; public readonly string ModVersion; - public readonly Md5Hash Hash; + public Md5Hash Hash { get; private set; } public readonly DateTime? InstallTime; - public readonly ImmutableArray Files; - public readonly ImmutableArray<(string error, string? stackTrace)> Errors; + public ImmutableArray Files { get; private set; } + public ImmutableArray Errors { get; private set; } public async Task IsUpToDate() { @@ -55,7 +55,7 @@ namespace Barotrauma /// /// Does the content package include some content that needs to match between all players in multiplayer. /// - public readonly bool HasMultiplayerSyncedContent; + public bool HasMultiplayerSyncedContent { get; private set; } protected ContentPackage(XDocument doc, string path) { @@ -84,13 +84,13 @@ namespace Barotrauma .ToArray(); Files = fileResults - .OfType>() + .OfType>() .Select(f => f.Value) .ToImmutableArray(); Errors = fileResults - .OfType>() - .Select(f => (f.Error, f.StackTrace)) + .OfType>() + .Select(f => f.Error) .ToImmutableArray(); HasMultiplayerSyncedContent = Files.Any(f => !f.NotSyncedInMultiplayer); @@ -127,18 +127,13 @@ namespace Barotrauma try { - if (doc.Root.GetAttributeBool("corepackage", false)) - { - return new CorePackage(doc, path); - } - else - { - return new RegularPackage(doc, path); - } + return doc.Root.GetAttributeBool("corepackage", false) + ? (ContentPackage)new CorePackage(doc, path) + : new RegularPackage(doc, path); } catch (Exception e) { - while (e.InnerException != null) { e = e.InnerException; } + e = e.GetInnermost(); DebugConsole.ThrowError($"{e.Message}: {e.StackTrace}"); return null; } @@ -278,12 +273,42 @@ namespace Barotrauma Files.ForEach(f => f.UnloadFile()); } - public override int GetHashCode() + public void ReloadSubsAndItemAssemblies() { - byte[] shortHash = Encoding.ASCII.GetBytes(Hash.StringRepresentation.Substring(0, 4)); - return (shortHash[0] << 24) | (shortHash[1] << 16) | (shortHash[2] << 8) | shortHash[3]; + XDocument doc = XMLExtensions.TryLoadXml(Path); + List newFileList = new List(); + XElement rootElement = doc.Root ?? throw new NullReferenceException("XML document is invalid: root element is null."); + + var fileResults = rootElement.Elements() + .Select(e => ContentFile.CreateFromXElement(this, e)) + .ToArray(); + + foreach (var result in fileResults) + { + switch (result) + { + case Success { Value: var file }: + if (file is BaseSubFile || file is ItemAssemblyFile) + { + newFileList.Add(file); + } + else + { + var existingFile = Files.FirstOrDefault(f => f.Path == file.Path); + newFileList.Add(existingFile ?? file); + } + break; + } + } + + UnloadFilesOfType(); + UnloadFilesOfType(); + Files = newFileList.ToImmutableArray(); + Hash = CalculateHash(); + LoadFilesOfType(); + LoadFilesOfType(); } - + public static bool PathAllowedAsLocalModFile(string path) { #if DEBUG @@ -305,21 +330,17 @@ namespace Barotrauma public void LogErrors() { - if (Errors.Any()) + if (!Errors.Any()) { - DebugConsole.AddWarning( - $"The following errors occurred while loading the content package\"{Name}\". The package might not work correctly.\n" + - string.Join('\n', Errors.Select(e => errorToStr(e.error, e.stackTrace)))); - static string errorToStr(string error, string? stackTrace) - { - string str = error; - if (stackTrace != null) - { - str += '\n' + stackTrace; - } - return str; - } + return; } + + DebugConsole.AddWarning( + $"The following errors occurred while loading the content package \"{Name}\". The package might not work correctly.\n" + + string.Join('\n', Errors.Select(errorToStr))); + + static string errorToStr(ContentFile.LoadError error) + => error.ToString(); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index 68a695516..1d11503a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -430,9 +430,9 @@ namespace Barotrauma public static void LoadVanillaFileList() { VanillaCorePackage = new CorePackage(XDocument.Load(VanillaFileList), VanillaFileList); - foreach ((string error, string? stackTrace) in VanillaCorePackage.Errors) + foreach (ContentFile.LoadError error in VanillaCorePackage.Errors) { - DebugConsole.ThrowError(error + (stackTrace == null ? string.Empty : '\n' + stackTrace)); + DebugConsole.ThrowError(error.ToString()); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs index f5dba0b3d..b184da625 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs @@ -27,7 +27,7 @@ namespace Barotrauma public string BaseUri => Element.BaseUri; - public XDocument Document => Element.Document ?? throw new NullReferenceException("XML element is invalid: document is null."); + public XDocument? Document => Element.Document; public ContentXElement? FirstElement() => Elements().FirstOrDefault(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 01084658f..3722751b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -1836,8 +1836,8 @@ namespace Barotrauma ThrowError($"No start item set identifier defined!"); return; } - AutoItemPlacer.StartItemSet = args[0].ToIdentifier(); - NewMessage($"Start item set changed to \"{AutoItemPlacer.StartItemSet}\""); + AutoItemPlacer.DefaultStartItemSet = args[0].ToIdentifier(); + NewMessage($"Start item set changed to \"{AutoItemPlacer.DefaultStartItemSet}\""); }, isCheat: false)); //"dummy commands" that only exist so that the server can give clients permissions to use them diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 7e634941e..8ef199011 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -131,21 +131,22 @@ namespace Barotrauma MaxAttachableCount, } + [Flags] public enum AbilityFlags { - None, - MustWalk, - ImmuneToPressure, - IgnoredByEnemyAI, - MoveNormallyWhileDragging, - CanTinker, - CanTinkerFabricatorsAndDeconstructors, - TinkeringPowersDevices, - GainSkillPastMaximum, - RetainExperienceForNewCharacter, - AllowSecondOrderedTarget, - PowerfulCPR, - AlwaysStayConscious, + None = 0, + MustWalk = 0x1, + ImmuneToPressure = 0x2, + IgnoredByEnemyAI = 0x4, + MoveNormallyWhileDragging = 0x8, + CanTinker = 0x10, + CanTinkerFabricatorsAndDeconstructors = 0x20, + TinkeringPowersDevices = 0x40, + GainSkillPastMaximum = 0x80, + RetainExperienceForNewCharacter = 0x100, + AllowSecondOrderedTarget = 0x200, + PowerfulCPR = 0x400, + AlwaysStayConscious = 0x800, } [Flags] @@ -156,9 +157,24 @@ namespace Barotrauma Both = Bot | Player } + public enum StartingBalanceAmount + { + Low, + Medium, + High, + } + + public enum GameDifficulty + { + Easy, + Medium, + Hard, + Hellish + } + public enum NumberType { Int, Float } -} \ 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 da7661209..d4d95ebbb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs @@ -80,7 +80,7 @@ namespace Barotrauma } if (campaign is MultiPlayerCampaign mpCampaign) { - mpCampaign.LastUpdateID++; + mpCampaign.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions); } if (prefab != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs index c871e45ab..c3030d6df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs @@ -64,8 +64,6 @@ namespace Barotrauma campaign.GetWallet(client).Give(Amount); } } - - ((MultiPlayerCampaign)campaign).LastUpdateID++; #else campaign.Wallet.Give(Amount); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index a05b0a183..8d17092d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -49,6 +49,9 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public Identifier SpawnPointTag { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Should we spawn the entity even when no spawn points with matching tags were found?")] + public bool RequireSpawnPointTag { get; set; } + private readonly HashSet targetModuleTags = new HashSet(); [Serialize("", IsPropertySaveable.Yes, "What outpost module tags does the entity prefer to spawn in.")] @@ -79,7 +82,7 @@ namespace Barotrauma public SpawnAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { - ignoreSpawnPointType = !element.Attributes().Any(a => a.Name.ToString().Equals("spawnpointtype", StringComparison.OrdinalIgnoreCase)); + ignoreSpawnPointType = element.GetAttribute("spawnpointtype") == null; } public override bool IsFinished(ref string goTo) @@ -110,22 +113,40 @@ namespace Barotrauma if (humanPrefab != null) { ISpatialEntity spawnPos = GetSpawnPos(); - Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos?.WorldPosition ?? Vector2.Zero, 100.0f), humanPrefab.GetCharacterInfo(), onSpawn: newCharacter => + if (spawnPos != null) { - if (newCharacter == null) { return; } - newCharacter.HumanPrefab = humanPrefab; - newCharacter.TeamID = CharacterTeamType.FriendlyNPC; - newCharacter.EnableDespawn = false; - humanPrefab.GiveItems(newCharacter, newCharacter.Submarine); - if (LootingIsStealing) + Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, 100.0f), humanPrefab.GetCharacterInfo(), onSpawn: newCharacter => { - foreach (Item item in newCharacter.Inventory.AllItems) + if (newCharacter == null) { return; } + newCharacter.HumanPrefab = humanPrefab; + newCharacter.TeamID = CharacterTeamType.FriendlyNPC; + newCharacter.EnableDespawn = false; + humanPrefab.GiveItems(newCharacter, newCharacter.Submarine); + if (LootingIsStealing) { - item.SpawnedInCurrentOutpost = true; - item.AllowStealing = false; + foreach (Item item in newCharacter.Inventory.AllItems) + { + item.SpawnedInCurrentOutpost = true; + item.AllowStealing = false; + } } - } - humanPrefab.InitializeCharacter(newCharacter, spawnPos); + humanPrefab.InitializeCharacter(newCharacter, spawnPos); + if (!TargetTag.IsEmpty && newCharacter != null) + { + ParentEvent.AddTarget(TargetTag, newCharacter); + } + spawnedEntity = newCharacter; + }); + } + } + } + else if (!SpeciesName.IsEmpty) + { + ISpatialEntity spawnPos = GetSpawnPos(); + if (spawnPos != null) + { + Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, 100.0f), onSpawn: newCharacter => + { if (!TargetTag.IsEmpty && newCharacter != null) { ParentEvent.AddTarget(TargetTag, newCharacter); @@ -134,20 +155,9 @@ namespace Barotrauma }); } } - else if (!SpeciesName.IsEmpty) - { - Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(GetSpawnPos()?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawn: newCharacter => - { - if (!TargetTag.IsEmpty && newCharacter != null) - { - ParentEvent.AddTarget(TargetTag, newCharacter); - } - spawnedEntity = newCharacter; - }); - } else if (!ItemIdentifier.IsEmpty) { - if (!(MapEntityPrefab.Find(null, identifier: ItemIdentifier) is ItemPrefab itemPrefab)) + if (!(MapEntityPrefab.FindByIdentifier(ItemIdentifier) is ItemPrefab itemPrefab)) { DebugConsole.ThrowError("Error in SpawnAction (item prefab \"" + ItemIdentifier + "\" not found)"); } @@ -178,7 +188,11 @@ namespace Barotrauma if (spawnInventory == null) { - Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(GetSpawnPos()?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawned: onSpawned); + ISpatialEntity spawnPos = GetSpawnPos(); + if (spawnPos != null) + { + Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, 100.0f), onSpawned: onSpawned); + } } else { @@ -244,10 +258,10 @@ namespace Barotrauma SpawnType? spawnPointType = null; if (!ignoreSpawnPointType) { spawnPointType = SpawnPointType; } - return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable()); + return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable(), requireTaggedSpawnPoint: RequireSpawnPointTag); } - public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable moduleFlags = null, IEnumerable spawnpointTags = null, bool asFarAsPossibleFromAirlock = false) + public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable moduleFlags = null, IEnumerable spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false) { List potentialSpawnPoints = spawnLocation switch { @@ -274,18 +288,24 @@ namespace Barotrauma if (spawnpointTags != null && spawnpointTags.Any()) { var spawnPoints = potentialSpawnPoints - .Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag))) - .Where(wp => wp.ConnectedDoor == null && !wp.isObstructed); + .Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && !wp.isObstructed)); - if (spawnPoints.Any()) + if (requireTaggedSpawnPoint || spawnPoints.Any()) { potentialSpawnPoints = spawnPoints.ToList(); } } - if (potentialSpawnPoints.Count == 0) + if (potentialSpawnPoints.None()) { - DebugConsole.ThrowError($"Could not find a spawn point for a SpawnAction (spawn location: {spawnLocation})"); + if (requireTaggedSpawnPoint && spawnpointTags != null && spawnpointTags.Any()) + { + DebugConsole.NewMessage($"Could not find a spawn point for a SpawnAction (spawn location: {spawnLocation} (tag: {string.Join(",", spawnpointTags)}), skipping.", color: Color.White); + } + else + { + DebugConsole.ThrowError($"Could not find a spawn point for a SpawnAction (spawn location: {spawnLocation})"); + } return null; } @@ -307,7 +327,7 @@ namespace Barotrauma validSpawnPoints = validSpawnPoints.Except(airlockSpawnPoints); } - if (!validSpawnPoints.Any()) + if (validSpawnPoints.None()) { DebugConsole.ThrowError($"Could not find a spawn point of the correct type for a SpawnAction (spawn location: {spawnLocation}, type: {spawnPointType}, module flags: {((moduleFlags == null || !moduleFlags.Any()) ? "none" : string.Join(", ", moduleFlags))})"); return potentialSpawnPoints.GetRandomUnsynced(); @@ -320,7 +340,7 @@ namespace Barotrauma } //if not trying to spawn at a tagged spawnpoint, favor spawnpoints without tags - if (spawnpointTags == null || !spawnpointTags.Any()) + if (spawnpointTags == null || spawnpointTags.None()) { var spawnPoints = validSpawnPoints.Where(wp => !wp.Tags.Any()); if (spawnPoints.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index c295eaa49..94b7d0c01 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -260,9 +260,15 @@ namespace Barotrauma throw new InvalidOperationException("Could not select EventManager settings (level not set)."); } + float extraDifficulty = 0; + if (GameMain.GameSession.Campaign?.Settings != null) + { + extraDifficulty = GameMain.GameSession.Campaign.Settings.ExtraEventManagerDifficulty; + } + float modifiedDifficulty = Math.Clamp(level.Difficulty + extraDifficulty, 0, 100); var suitableSettings = EventManagerSettings.OrderedByDifficulty.Where(s => - level.Difficulty >= s.MinLevelDifficulty && - level.Difficulty <= s.MaxLevelDifficulty).ToArray(); + modifiedDifficulty >= s.MinLevelDifficulty && + modifiedDifficulty <= s.MaxLevelDifficulty).ToArray(); if (suitableSettings.Length == 0) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 66bd8857f..7ca86a794 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -437,7 +437,7 @@ namespace Barotrauma { minDistance = 5000; } - else if (SpawnPosType.HasFlag(Level.PositionType.Wreck)) + else if (SpawnPosType.HasFlag(Level.PositionType.Wreck) || SpawnPosType.HasFlag(Level.PositionType.BeaconStation)) { minDistance = 3000; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 5d15cbed9..7764c444e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -10,7 +10,7 @@ namespace Barotrauma { public static bool OutputDebugInfo = false; - public static void SpawnItems() + public static void SpawnItems(Identifier? startItemSet = null) { if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; } @@ -23,7 +23,7 @@ namespace Barotrauma var sub = Submarine.MainSubs[i]; if (sub == null || sub.Info.InitialSuppliesSpawned || !sub.Info.IsPlayer) { continue; } //1st pass: items defined in the start item set, only spawned in the main sub (not drones/shuttles or other linked subs) - SpawnStartItems(sub); + SpawnStartItems(sub, startItemSet); //2nd pass: items defined using preferred containers, spawned in the main sub and all the linked subs (drones, shuttles etc) var subs = sub.GetConnectedSubs().Where(s => s.TeamID == sub.TeamID); CreateAndPlace(subs); @@ -62,17 +62,23 @@ namespace Barotrauma CreateAndPlace(sub.ToEnumerable(), regeneratedContainer: regeneratedContainer); } - public static Identifier StartItemSet = new Identifier("normal"); + public static Identifier DefaultStartItemSet = new Identifier("normal"); /// /// Spawns the items defined in the start item set in the specified sub. /// - private static void SpawnStartItems(Submarine sub) + private static void SpawnStartItems(Submarine sub, Identifier? startItemSet) { - if (!Barotrauma.StartItemSet.Sets.TryGet(StartItemSet, out StartItemSet itemSet)) + Identifier setIdentifier = startItemSet ?? DefaultStartItemSet; + if (!StartItemSet.Sets.TryGet(setIdentifier, out StartItemSet itemSet)) { - DebugConsole.AddWarning($"Couldn't find a start item set matching the identifier \"{StartItemSet}\"!"); - return; + DebugConsole.AddWarning($"Couldn't find a start item set matching the identifier \"{setIdentifier}\"!"); + if (!StartItemSet.Sets.TryGet(DefaultStartItemSet, out StartItemSet defaultSet)) + { + DebugConsole.ThrowError($"Couldn't find the default start item set \"{DefaultStartItemSet}\"!"); + return; + } + itemSet = defaultSet; } WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, sub); ISpatialEntity initialSpawnPos; @@ -164,7 +170,7 @@ namespace Barotrauma var itemPrefabs = ItemPrefab.Prefabs.OrderBy(p => p.UintIdentifier); foreach (ItemPrefab ip in itemPrefabs) { - if (!ip.PreferredContainers.Any()) { continue; } + if (ip.PreferredContainers.None()) { continue; } if (ip.ConfigElement.Elements().Any(e => string.Equals(e.Name.ToString(), typeof(ItemContainer).Name.ToString(), StringComparison.OrdinalIgnoreCase)) && itemPrefabs.Any(ip2 => CanSpawnIn(ip2, ip))) { prefabsItemsCanSpawnIn.Add(ip); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 04ca98e30..717951a59 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -10,63 +10,6 @@ using System.Xml.Linq; namespace Barotrauma { - internal struct CampaignSettings - { - public static CampaignSettings Empty => new CampaignSettings(); - - // Anything that uses this field I wasn't sure if actually needed the proper campaign settings to be passed down - public static CampaignSettings Unsure => Empty; - public bool RadiationEnabled { get; set; } - - public int TotalMaxMissionCount => MaxMissionCount + GetAddedMissionCount(); - - private int maxMissionCount; - public int MaxMissionCount - { - get { return maxMissionCount; } - set { maxMissionCount = MathHelper.Clamp(value, MinMissionCountLimit, MaxMissionCountLimit); } - } - - public const int DefaultMaxMissionCount = 2; - public const int MaxMissionCountLimit = 10; - public const int MinMissionCountLimit = 1; - - public CampaignSettings(IReadMessage inc) - { - maxMissionCount = DefaultMaxMissionCount; - RadiationEnabled = inc.ReadBoolean(); - MaxMissionCount = inc.ReadRangedInteger(MinMissionCountLimit, MaxMissionCountLimit); - } - - public CampaignSettings(XElement element) - { - maxMissionCount = DefaultMaxMissionCount; - RadiationEnabled = element.GetAttributeBool(nameof(RadiationEnabled).ToLowerInvariant(), true); - MaxMissionCount = element.GetAttributeInt(nameof(MaxMissionCount).ToLowerInvariant(), DefaultMaxMissionCount); - } - - public void Serialize(IWriteMessage msg) - { - msg.Write(RadiationEnabled); - msg.WriteRangedInteger(MaxMissionCount, MinMissionCountLimit, MaxMissionCountLimit); - } - - public int GetAddedMissionCount() - { - int count = 0; - foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) - { - count += (int)character.GetStatValue(StatTypes.ExtraMissionCount); - } - return count; - } - - public XElement Save() - { - return new XElement(nameof(CampaignSettings), new XAttribute(nameof(RadiationEnabled).ToLowerInvariant(), RadiationEnabled), new XAttribute(nameof(MaxMissionCount).ToLowerInvariant(), MaxMissionCount)); - } - } - abstract partial class CampaignMode : GameMode { [NetworkSerialize] @@ -149,9 +92,8 @@ namespace Barotrauma //key = dialog flag, double = Timing.TotalTime when the line was last said private readonly Dictionary dialogLastSpoken = new Dictionary(); - public bool PurchasedHullRepairs, PurchasedLostShuttles, PurchasedItemRepairs; - public SubmarineInfo PendingSubmarineSwitch; + public bool TransferItemsOnSubSwitch { get; set; } protected Map map; public Map Map @@ -189,12 +131,16 @@ namespace Barotrauma protected set; } - protected CampaignMode(GameModePreset preset) + public virtual bool PurchasedHullRepairs { get; set; } + public virtual bool PurchasedLostShuttles { get; set; } + public virtual bool PurchasedItemRepairs { get; set; } + + protected CampaignMode(GameModePreset preset, CampaignSettings settings) : base(preset) { Bank = new Wallet(Option.None()) { - Balance = InitialMoney + Balance = settings.InitialMoney }; CargoManager = new CargoManager(this); @@ -596,6 +542,7 @@ namespace Barotrauma if (Level.Loaded.StartOutpost == null) { Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + if (closestSub == null) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } else @@ -729,7 +676,6 @@ namespace Barotrauma } } - public void EndCampaign() { foreach (Character c in Character.CharacterList) @@ -741,7 +687,7 @@ namespace Barotrauma } foreach (LocationConnection connection in Map.Connections) { - connection.Difficulty = MathHelper.Lerp(connection.Difficulty, 100.0f, 0.25f); + connection.Difficulty = connection.Biome.MaxDifficulty; connection.LevelData = new LevelData(connection) { IsBeaconActive = false @@ -750,6 +696,7 @@ namespace Barotrauma } foreach (Location location in Map.Locations) { + location.LevelData = new LevelData(location, location.Biome.MaxDifficulty); location.Reset(); } Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation)); @@ -873,7 +820,7 @@ namespace Barotrauma const float MaxDist = 3000.0f; const float MinDist = 2500.0f; - if (!Level.IsLoadedOutpost) { return; } + if (!Level.IsLoadedFriendlyOutpost) { return; } Rectangle worldBorders = Submarine.MainSub.GetDockedBorders(); worldBorders.Location += Submarine.MainSub.WorldPosition.ToPoint(); @@ -1058,7 +1005,10 @@ namespace Barotrauma public SubmarineInfo SwitchSubs() { - TransferItemsBetweenSubs(); + if (TransferItemsOnSubSwitch) + { + TransferItemsBetweenSubs(); + } RefreshOwnedSubmarines(); PendingSubmarineSwitch = null; return GameMain.GameSession.SubmarineInfo; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignModePresets.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignModePresets.cs new file mode 100644 index 000000000..430bb063a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignModePresets.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Xml.Linq; + +namespace Barotrauma +{ + internal static class CampaignModePresets + { + public static readonly ImmutableArray List; + public static readonly ImmutableDictionary Definitions; + + private static readonly string fileListPath = Path.Combine("Data", "campaignsettings.xml"); + + static CampaignModePresets() + { + if (!File.Exists(fileListPath) || !(XMLExtensions.TryLoadXml(fileListPath)?.Root is { } docRoot)) + { + List = ImmutableArray.Empty; + return; + } + + List list = new List(); + Dictionary definitions = new Dictionary(); + + foreach (XElement element in docRoot.Elements()) + { + Identifier name = element.NameAsIdentifier(); + + if (name == CampaignSettings.LowerCaseSaveElementName) + { + list.Add(new CampaignSettings(element)); + } + else if (name == nameof(CampaignSettingDefinitions)) + { + foreach (XElement subElement in element.Elements()) + { + definitions.Add(subElement.NameAsIdentifier(), new CampaignSettingDefinitions(subElement)); + } + } + } + + List = list.ToImmutableArray(); + Definitions = definitions.ToImmutableDictionary(); + } + } + + internal readonly struct CampaignSettingDefinitions + { + // Definitely not the best way to do this + private readonly ImmutableDictionary> values; + + 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) + { + return values.TryGetValue(identifier, out Either value) && value.TryGet(out float range) ? range : 0.0f; + } + + public int GetInt(Identifier identifier) + { + return values.TryGetValue(identifier, out Either value) && value.TryGet(out int integer) ? integer : 0; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs new file mode 100644 index 000000000..1d96fa2fa --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs @@ -0,0 +1,114 @@ +#nullable enable + +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma +{ + internal class CampaignSettings : INetSerializableStruct, ISerializableEntity + { + public static CampaignSettings Empty => new CampaignSettings(element: null); + + public string Name => "CampaignSettings"; + + public const string LowerCaseSaveElementName = "campaignsettings"; + + [Serialize("", IsPropertySaveable.Yes)] + public string PresetName { get; set; } = string.Empty; + + [Serialize(false, IsPropertySaveable.Yes), NetworkSerialize] + public bool RadiationEnabled { get; set; } + + private int maxMissionCount; + + [Serialize(DefaultMaxMissionCount, IsPropertySaveable.Yes), NetworkSerialize(MinValueInt = MinMissionCountLimit, MaxValueInt = MaxMissionCountLimit)] + public int MaxMissionCount + { + get => maxMissionCount; + set => maxMissionCount = MathHelper.Clamp(value, MinMissionCountLimit, MaxMissionCountLimit); + } + + 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("normal", IsPropertySaveable.Yes), NetworkSerialize] + public Identifier StartItemSet { get; set; } + + public int InitialMoney + { + get + { + if (CampaignModePresets.Definitions.TryGetValue(nameof(StartingBalanceAmount).ToIdentifier(), out var definition)) + { + return definition.GetInt(StartingBalanceAmount.ToIdentifier()); + } + return 8000; + + } + } + + public float ExtraEventManagerDifficulty + { + get + { + if (CampaignModePresets.Definitions.TryGetValue(nameof(ExtraEventManagerDifficulty).ToIdentifier(), out var definition)) + { + return definition.GetFloat(Difficulty.ToIdentifier()); + } + return 0; + } + } + + public float LevelDifficultyMultiplier + { + get + { + if (CampaignModePresets.Definitions.TryGetValue(nameof(LevelDifficultyMultiplier).ToIdentifier(), out var definition)) + { + return definition.GetFloat(Difficulty.ToIdentifier()); + } + return 1.0f; + } + } + + public const int DefaultMaxMissionCount = 2; + public const int MaxMissionCountLimit = 10; + public const int MinMissionCountLimit = 1; + + public Dictionary SerializableProperties { get; private set; } + + // required for INetSerializableStruct + public CampaignSettings() + { + SerializableProperties = SerializableProperty.GetProperties(this); + } + + public CampaignSettings(XElement? element = null) + { + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + } + + public XElement Save() + { + XElement saveElement = new XElement(LowerCaseSaveElementName); + SerializableProperty.SerializeProperties(this, saveElement, saveIfDefault: true); + return saveElement; + } + + private static int GetAddedMissionCount() + { + int count = 0; + foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + { + count += (int)character.GetStatValue(StatTypes.ExtraMissionCount); + } + return count; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs index d8871d2bc..789a31c8f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs @@ -29,7 +29,11 @@ namespace Barotrauma : base(preset) { Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation }; - missions.Add(Mission.LoadRandom(locations, seed, false, missionType)); + var mission = Mission.LoadRandom(locations, seed, false, missionType); + if (mission != null) + { + missions.Add(mission); + } } protected static IEnumerable ValidateMissionPrefabs(IEnumerable missionPrefabs, Dictionary missionClasses) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 181c73232..18a0f6fcf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -12,19 +12,60 @@ namespace Barotrauma { public const int MinimumInitialMoney = 500; - private UInt16 lastUpdateID; - public UInt16 LastUpdateID + [Flags] + public enum NetFlags : UInt16 { - get - { -#if SERVER - if (GameMain.Server != null && lastUpdateID < 1) { lastUpdateID++; } -#endif - return lastUpdateID; - } - set { lastUpdateID = value; } + Misc = 0x1, + MapAndMissions = 0x2, + UpgradeManager = 0x4, + SubList = 0x8, + ItemsInBuyCrate = 0x10, + ItemsInSellFromSubCrate = 0x20, + PurchasedItems = 0x80, + SoldItems = 0x100, + Reputation = 0x200, + CharacterInfo = 0x800 } + private readonly Dictionary lastUpdateID; + + public UInt16 GetLastUpdateIdForFlag(NetFlags flag) + { + if (!ValidateFlag(flag)) { return 0; } + return lastUpdateID[flag]; + } + public void SetLastUpdateIdForFlag(NetFlags flag, UInt16 id) + { + if (!ValidateFlag(flag)) { return; } + lastUpdateID[flag] = id; + } + + public void IncrementLastUpdateIdForFlag(NetFlags flag) + { + if (!ValidateFlag(flag)) { return; } + if (!lastUpdateID.ContainsKey(flag)) { lastUpdateID[flag] = 0; } + lastUpdateID[flag]++; + } + public void IncrementAllLastUpdateIds() + { + foreach (NetFlags flag in Enum.GetValues(typeof(NetFlags))) + { + if (!lastUpdateID.ContainsKey(flag)) { lastUpdateID[flag] = 0; } + lastUpdateID[flag]++; + } + } + + private bool ValidateFlag(NetFlags flag) + { + if (MathHelper.IsPowerOfTwo((int)flag)) { return true; } +#if DEBUG + throw new InvalidOperationException($"\"{flag}\" is not a valid campaign update flag."); +#else + return false; +#endif + } + + private UInt16 lastSaveID; public UInt16 LastSaveID { @@ -35,11 +76,11 @@ namespace Barotrauma #endif return lastSaveID; } - set + set { #if SERVER //trigger a campaign update to notify the clients of the changed save ID - lastUpdateID++; + IncrementLastUpdateIdForFlag(NetFlags.Misc); #endif lastSaveID = value; } @@ -52,23 +93,33 @@ namespace Barotrauma get; set; } - private MultiPlayerCampaign() : base(GameModePreset.MultiPlayerCampaign) + private MultiPlayerCampaign(CampaignSettings settings) : base(GameModePreset.MultiPlayerCampaign, settings) { currentCampaignID++; + lastUpdateID = new Dictionary(); + foreach (NetFlags flag in Enum.GetValues(typeof(NetFlags))) + { +#if SERVER + //server starts from a higher ID to ensure we send the initial state + lastUpdateID[flag] = 1; +#else + lastUpdateID[flag] = 0; +#endif + } CampaignID = currentCampaignID; CampaignMetadata = new CampaignMetadata(this); UpgradeManager = new UpgradeManager(this); InitCampaignData(); } - public static MultiPlayerCampaign StartNew(string mapSeed, SubmarineInfo selectedSub, CampaignSettings settings) + public static MultiPlayerCampaign StartNew(string mapSeed, CampaignSettings settings) { - MultiPlayerCampaign campaign = new MultiPlayerCampaign(); + MultiPlayerCampaign campaign = new MultiPlayerCampaign(settings); //only the server generates the map, the clients load it from a save file if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - campaign.map = new Map(campaign, mapSeed, settings); campaign.Settings = settings; + campaign.map = new Map(campaign, mapSeed); } campaign.InitProjSpecific(); return campaign; @@ -76,7 +127,7 @@ namespace Barotrauma public static MultiPlayerCampaign LoadNew(XElement element) { - MultiPlayerCampaign campaign = new MultiPlayerCampaign(); + MultiPlayerCampaign campaign = new MultiPlayerCampaign(CampaignSettings.Empty); campaign.Load(element); campaign.InitProjSpecific(); campaign.IsFirstRound = false; @@ -124,18 +175,17 @@ namespace Barotrauma { switch (subElement.Name.ToString().ToLowerInvariant()) { - case "campaignsettings": + case CampaignSettings.LowerCaseSaveElementName: Settings = new CampaignSettings(subElement); #if CLIENT - GameMain.NetworkMember.ServerSettings.MaxMissionCount = Settings.MaxMissionCount; - GameMain.NetworkMember.ServerSettings.RadiationEnabled = Settings.RadiationEnabled; + GameMain.NetworkMember.ServerSettings.CampaignSettings = Settings; #endif break; case "map": if (map == null) { //map not created yet, loading this campaign for the first time - map = Map.Load(this, subElement, Settings); + map = Map.Load(this, subElement); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 42a9dc8d0..fe1f4fad2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -72,7 +72,7 @@ namespace Barotrauma get { if (Map != null) { return Map.CurrentLocation; } - if (dummyLocations == null) { CreateDummyLocations(); } + if (dummyLocations == null) { dummyLocations = CreateDummyLocations(LevelData?.Seed ?? string.Empty); } if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); } return dummyLocations[0]; } @@ -83,7 +83,7 @@ namespace Barotrauma get { if (Map != null) { return Map.SelectedLocation; } - if (dummyLocations == null) { CreateDummyLocations(); } + if (dummyLocations == null) { dummyLocations = CreateDummyLocations(LevelData?.Seed ?? string.Empty); } if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); } return dummyLocations[1]; } @@ -207,7 +207,7 @@ namespace Barotrauma } else if (gameModePreset.GameModeType == typeof(MultiPlayerCampaign)) { - var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings); + var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), settings); if (selectedSub != null) { campaign.Bank.Deduct(selectedSub.Price); @@ -218,7 +218,7 @@ namespace Barotrauma #if CLIENT else if (gameModePreset.GameModeType == typeof(SinglePlayerCampaign)) { - var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings); + var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), settings); if (selectedSub != null) { campaign.Bank.TryDeduct(selectedSub.Price); @@ -245,25 +245,15 @@ namespace Barotrauma } } - private void CreateDummyLocations(LocationType? forceLocationType = null) + public static Location[] CreateDummyLocations(string seed, LocationType? forceLocationType = null) { - dummyLocations = new Location[2]; - - string seed = ""; - if (GameMain.GameSession != null && GameMain.GameSession.Level != null) - { - seed = GameMain.GameSession.Level.Seed; - } - else if (GameMain.NetLobbyScreen != null) - { - seed = GameMain.NetLobbyScreen.LevelSeed; - } - + var dummyLocations = new Location[2]; MTRandom rand = new MTRandom(ToolBox.StringToInt(seed)); for (int i = 0; i < 2; i++) { dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand, requireOutpost: true, forceLocationType: forceLocationType); } + return dummyLocations; } public void LoadPreviousSave() @@ -275,7 +265,7 @@ namespace Barotrauma /// /// Switch to another submarine. The sub is loaded when the next round starts. /// - public void SwitchSubmarine(SubmarineInfo newSubmarine, int cost, Client? client = null) + public void SwitchSubmarine(SubmarineInfo newSubmarine, bool transferItems, int cost, Client? client = null) { if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name)) { @@ -299,6 +289,7 @@ namespace Barotrauma } GameAnalyticsManager.AddMoneySpentEvent(cost, GameAnalyticsManager.MoneySink.SubmarineSwitch, newSubmarine.Name); Campaign!.PendingSubmarineSwitch = newSubmarine; + Campaign!.TransferItemsOnSubSwitch = transferItems; } public void PurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null) @@ -309,6 +300,9 @@ namespace Barotrauma { GameAnalyticsManager.AddMoneySpentEvent(newSubmarine.Price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name); OwnedSubmarines.Add(newSubmarine); +#if SERVER + (Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.SubList); +#endif } } @@ -345,7 +339,7 @@ namespace Barotrauma !missionPrefab.AllowedConnectionTypes.Any()) { LocationType? locationType = LocationType.Prefabs.FirstOrDefault(lt => missionPrefab.AllowedLocationTypes.Any(m => m == lt.Identifier)); - CreateDummyLocations(locationType); + dummyLocations = CreateDummyLocations(levelSeed, locationType); randomLevel = LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams, requireOutpost: true); break; } @@ -430,7 +424,7 @@ namespace Barotrauma Level? level = null; if (levelData != null) { - level = Level.Generate(levelData, mirrorLevel, startOutpost, endOutpost); + level = Level.Generate(levelData, mirrorLevel, StartLocation, EndLocation, startOutpost, endOutpost); } InitializeLevel(level); @@ -603,7 +597,7 @@ namespace Barotrauma Level.SpawnCorpses(); Level.PrepareBeaconStation(); } - AutoItemPlacer.SpawnItems(); + AutoItemPlacer.SpawnItems(Campaign?.Settings.StartItemSet); } if (GameMode is MultiPlayerCampaign mpCampaign) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index cb24eb337..a660bbbf3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -360,9 +360,6 @@ namespace Barotrauma { if (allowedSlot.HasFlag(SlotTypes[i]) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) && slots[i].Items.Any(it => it != item)) { -#if CLIENT - if (PersonalSlots.HasFlag(SlotTypes[i])) { hidePersonalSlots = false; } -#endif if (!slots[i].First().AllowedSlots.Contains(InvSlotType.Any) || !TryPutItem(slots[i].FirstOrDefault(), character, new List { InvSlotType.Any }, true, ignoreCondition)) { free = false; @@ -382,9 +379,6 @@ namespace Barotrauma { if (allowedSlot.HasFlag(SlotTypes[i]) && item.GetComponents().Any(p => p.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i]))) && slots[i].Empty()) { -#if CLIENT - if (PersonalSlots.HasFlag(SlotTypes[i])) { hidePersonalSlots = false; } -#endif bool removeFromOtherSlots = item.ParentInventory != this; if (placedInSlot == -1 && inWrongSlot) { @@ -454,9 +448,6 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("CharacterInventory.TryPutItem:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return false; } -#if CLIENT - if (PersonalSlots.HasFlag(SlotTypes[index])) { hidePersonalSlots = false; } -#endif //there's already an item in the slot if (slots[index].Any()) { @@ -480,9 +471,6 @@ namespace Barotrauma foreach (InvSlotType allowedSlot in pickable.AllowedSlots) { if (!allowedSlot.HasFlag(SlotTypes[index])) { continue; } - #if CLIENT - if (PersonalSlots.HasFlag(allowedSlot)) { hidePersonalSlots = false; } - #endif for (int i = 0; i < capacity; i++) { if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Any() && !slots[i].Contains(item)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs index d8543526e..059832004 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs @@ -144,9 +144,16 @@ namespace Barotrauma.Items.Components } } - public bool Combine(GeneticMaterial otherGeneticMaterial, Character user) + public enum CombineResult { - if (!CanBeCombinedWith(otherGeneticMaterial)) { return false; } + None, + Refined, + Combined + } + + public CombineResult Combine(GeneticMaterial otherGeneticMaterial, Character user) + { + if (!CanBeCombinedWith(otherGeneticMaterial)) { return CombineResult.None; } float conditionIncrease = Rand.Range(ConditionIncreaseOnCombineMin, ConditionIncreaseOnCombineMax); conditionIncrease += user?.GetStatValue(StatTypes.GeneticMaterialRefineBonus) ?? 0.0f; @@ -158,7 +165,7 @@ namespace Barotrauma.Items.Components { MakeTainted(); } - return true; + return CombineResult.Refined; } else { @@ -171,7 +178,7 @@ namespace Barotrauma.Items.Components { MakeTainted(); } - return false; + return CombineResult.Combined; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 7a624ac27..c615ae481 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -697,14 +697,17 @@ namespace Barotrauma.Items.Components Vector2 fromCharacterToLeak = leak.WorldPosition - character.AnimController.AimSourceWorldPos; float dist = fromCharacterToLeak.Length(); float reach = AIObjectiveFixLeak.CalculateReach(this, character); - - if (dist > reach * 3) + if (dist > reach * 2) { // Too far away -> consider this done and hope the AI is smart enough to move closer Reset(); return true; } character.AIController.SteeringManager.Reset(); + if (character.AIController.SteeringManager is IndoorsSteeringManager pathSteering) + { + pathSteering.ResetPath(); + } if (!character.AnimController.InWater) { // TODO: use the collider size? @@ -714,34 +717,25 @@ namespace Barotrauma.Items.Components humanAnim.Crouching = true; } } - if (dist > reach * 0.8f || dist > reach * 0.5f && character.AnimController.Limbs.Any(l => l.InWater)) + if (!character.IsClimbing) { - // Steer closer - if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering) + if (dist > reach * 0.8f || dist > reach * 0.5f && character.AnimController.Limbs.Any(l => l.InWater)) { - // Swimming inside the sub - if (indoorSteering.CurrentPath != null && !indoorSteering.IsPathDirty && (indoorSteering.CurrentPath.Unreachable || indoorSteering.CurrentPath.Finished)) + // Steer closer + Vector2 dir = Vector2.Normalize(fromCharacterToLeak); + if (!character.InWater) { - Vector2 dir = Vector2.Normalize(fromCharacterToLeak); - character.AIController.SteeringManager.SteeringManual(deltaTime, dir); - } - else - { - character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak)); + dir.Y = 0; } + character.AIController.SteeringManager.SteeringManual(deltaTime, dir); } - else + else if (dist < reach * 0.25f && !character.IsClimbing) { - // Swimming outside the sub - character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak)); + // Too close -> steer away + character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition)); } } - else if (dist < reach * 0.25f) - { - // Too close -> steer away - character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition)); - } - if (dist <= reach) + if (dist <= reach || character.IsClimbing) { // In range character.CursorPosition = leak.WorldPosition; @@ -815,7 +809,7 @@ namespace Barotrauma.Items.Components } bool leakFixed = (leak.Open <= 0.0f || leak.Removed) && - (leak.ConnectedWall == null || leak.ConnectedWall.Sections.Average(s => s.damage) < 1); + (leak.ConnectedWall == null || leak.ConnectedWall.Sections.Max(s => s.damage) < 0.1f); if (leakFixed && leak.FlowTargetHull?.DisplayName != null && character.IsOnPlayerTeam) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 903905ec6..e179980be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -187,7 +187,12 @@ namespace Barotrauma.Items.Components [Serialize(false, IsPropertySaveable.No)] public bool RemoveContainedItemsOnDeconstruct { get; set; } - private SlotRestrictions[] slotRestrictions; + private readonly ImmutableArray slotRestrictions; + + readonly List targets = new List(); + + private Vector2 prevContainedItemPositions; + public bool ShouldBeContained(string[] identifiersOrTags, out bool isRestrictionsDefined) { @@ -237,10 +242,11 @@ namespace Barotrauma.Items.Components } } Inventory = new ItemInventory(item, this, totalCapacity, SlotsPerRow); - slotRestrictions = new SlotRestrictions[totalCapacity]; + + List newSlotRestrictions = new List(totalCapacity); for (int i = 0; i < capacity; i++) { - slotRestrictions[i] = new SlotRestrictions(maxStackSize, ContainableItems); + newSlotRestrictions.Add(new SlotRestrictions(maxStackSize, ContainableItems)); } int subContainerIndex = capacity; @@ -268,11 +274,13 @@ namespace Barotrauma.Items.Components for (int i = subContainerIndex; i < subContainerIndex + subCapacity; i++) { - slotRestrictions[i] = new SlotRestrictions(subMaxStackSize, subContainableItems); + newSlotRestrictions.Add(new SlotRestrictions(subMaxStackSize, subContainableItems)); } subContainerIndex += subCapacity; } capacity = totalCapacity; + slotRestrictions = newSlotRestrictions.ToImmutableArray(); + System.Diagnostics.Debug.Assert(totalCapacity == slotRestrictions.Length); InitProjSpecific(element); } @@ -365,18 +373,21 @@ namespace Barotrauma.Items.Components return false; } - readonly List targets = new List(); - public override void Update(float deltaTime, Camera cam) { if (!string.IsNullOrEmpty(SpawnWithId) && !alwaysContainedItemsSpawned) { SpawnAlwaysContainedItems(); + alwaysContainedItemsSpawned = true; } if (item.ParentInventory is CharacterInventory ownerInventory) { - item.SetContainedItemPositions(); + if (Vector2.DistanceSquared(prevContainedItemPositions, item.Position) > 10.0f) + { + SetContainedItemPositions(); + prevContainedItemPositions = item.Position; + } if (AutoInject) { @@ -397,7 +408,7 @@ namespace Barotrauma.Items.Components item.body.Enabled && item.body.FarseerBody.Awake) { - item.SetContainedItemPositions(); + SetContainedItemPositions(); } else if (activeContainedItems.Count == 0) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 4bf9d88fe..adda6abc2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -147,7 +147,7 @@ namespace Barotrauma.Items.Components CancelUsing(user); user = null; } - if (!IsToggle) { IsActive = false; } + if (!IsToggle || item.Connections == null) { IsActive = false; } return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index c09526555..c4f04b034 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -231,28 +231,40 @@ namespace Barotrauma.Items.Components if (targetItem == otherItem) { continue; } if (deconstructProduct.RequiredOtherItem.Any(r => otherItem.HasTag(r) || r == otherItem.Prefab.Identifier)) { - user?.CheckTalents(AbilityEffectType.OnGeneticMaterialCombinedOrRefined); - foreach (Character character in Character.GetFriendlyCrew(user)) - { - character.CheckTalents(AbilityEffectType.OnCrewGeneticMaterialCombinedOrRefined); - } - var geneticMaterial1 = targetItem.GetComponent(); var geneticMaterial2 = otherItem.GetComponent(); if (geneticMaterial1 != null && geneticMaterial2 != null) { - if (geneticMaterial1.Combine(geneticMaterial2, user)) + var result = geneticMaterial1.Combine(geneticMaterial2, user); + if (result == GeneticMaterial.CombineResult.Refined) { inputContainer.Inventory.RemoveItem(otherItem); OutputContainer.Inventory.RemoveItem(otherItem); Entity.Spawner.AddItemToRemoveQueue(otherItem); } + if (result != GeneticMaterial.CombineResult.None) + { + OnCombinedOrRefined(); + } allowRemove = false; return; } - inputContainer.Inventory.RemoveItem(otherItem); - OutputContainer.Inventory.RemoveItem(otherItem); - Entity.Spawner.AddItemToRemoveQueue(otherItem); + else + { + inputContainer.Inventory.RemoveItem(otherItem); + OutputContainer.Inventory.RemoveItem(otherItem); + Entity.Spawner.AddItemToRemoveQueue(otherItem); + OnCombinedOrRefined(); + } + } + } + + void OnCombinedOrRefined() + { + user?.CheckTalents(AbilityEffectType.OnGeneticMaterialCombinedOrRefined); + foreach (Character character in Character.GetFriendlyCrew(user)) + { + character.CheckTalents(AbilityEffectType.OnCrewGeneticMaterialCombinedOrRefined); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 7e3e1da2e..a04b1eb25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -400,8 +400,8 @@ namespace Barotrauma.Items.Components private void IncreaseSkillLevel(Character user, float deltaTime) { if (user?.Info == null) { return; } - // Do not increase the helm skill when "steering" the sub in an outpost level - if (GameMain.GameSession?.Campaign != null && Level.IsLoadedOutpost) { 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; } float userSkill = Math.Max(user.GetSkillLevel("helm"), 1.0f) / 100.0f; user.Info.IncreaseSkillLevel( diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs index ae166ebf5..24a780d6e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Xml.Linq; using Microsoft.Xna.Framework; namespace Barotrauma.Items.Components @@ -81,6 +80,8 @@ namespace Barotrauma.Items.Components private ItemContainer? container; private float growthTickTimer; + private List? lightComponents; + public Planter(Item item, ContentXElement element) : base(item, element) { canBePicked = true; @@ -107,10 +108,14 @@ namespace Barotrauma.Items.Components base.OnItemLoaded(); IsActive = true; #if CLIENT - lightComponent = item.GetComponent(); - if (lightComponent != null) + var lights = item.GetComponents(); + if (lights.Any()) { - lightComponent.Light.Enabled = false; + lightComponents = lights.ToList(); + foreach (var light in lightComponents) + { + light.Light.Enabled = false; + } } #endif container = item.GetComponent(); @@ -227,12 +232,17 @@ namespace Barotrauma.Items.Components base.Update(deltaTime, cam); #if CLIENT - if (lightComponent != null) + if (lightComponents != null && lightComponents.Count > 0) { bool hasSeed = false; - foreach (Growable? seed in GrowableSeeds) { hasSeed |= seed != null; } - - lightComponent.Light.Enabled = hasSeed; + foreach (Growable? seed in GrowableSeeds) + { + hasSeed |= seed != null; + } + foreach (var light in lightComponents) + { + light.Light.Enabled = hasSeed; + } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index edb4d6242..7f8fc6789 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -602,7 +602,7 @@ namespace Barotrauma.Items.Components private bool ShouldDeteriorate() { - if (Level.IsLoadedOutpost) { return false; } + if (Level.IsLoadedFriendlyOutpost) { return false; } if (LastActiveTime > Timing.TotalTime) { return true; } foreach (ItemComponent ic in item.Components) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index 6cbb632a9..e278206b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -13,12 +13,13 @@ namespace Barotrauma.Items.Components private float updateTimer; + [Flags] public enum TargetType { - Any, - Human, - Monster, - Wall + Human = 1, + Monster = 2, + Wall = 4, + Any = Human | Monster | Wall, } [Serialize(false, IsPropertySaveable.No, description: "Has the item currently detected movement. Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")] @@ -179,6 +180,11 @@ namespace Barotrauma.Items.Components if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(new Signal(signalOut, 1), "state_out"); } + if (MotionDetected) + { + ApplyStatusEffects(ActionType.OnUse, deltaTime); + } + updateTimer -= deltaTime; if (updateTimer > 0.0f) { return; } @@ -199,8 +205,7 @@ namespace Barotrauma.Items.Components float broadRangeX = Math.Max(rangeX * 2, 500); float broadRangeY = Math.Max(rangeY * 2, 500); - if (item.CurrentHull == null && item.Submarine != null && - (Target == TargetType.Wall || Target == TargetType.Any)) + if (item.CurrentHull == null && item.Submarine != null && Target.HasFlag(TargetType.Wall)) { if (Level.Loaded != null && (Math.Abs(item.Submarine.Velocity.X) > MinimumVelocity || Math.Abs(item.Submarine.Velocity.Y) > MinimumVelocity)) { @@ -248,7 +253,7 @@ namespace Barotrauma.Items.Components } } - if (Target != TargetType.Wall) + if (Target.HasFlag(TargetType.Human) || Target.HasFlag(TargetType.Monster)) { foreach (Character c in Character.CharacterList) { @@ -258,14 +263,13 @@ namespace Barotrauma.Items.Components //makes it possible to detect when a spawned character moves without triggering the detector immediately as the ragdoll spawns and drops to the ground if (c.SpawnTime > Timing.TotalTime - 1.0) { continue; } - switch (Target) + if (c.IsHuman) { - case TargetType.Human: - if (!c.IsHuman) { continue; } - break; - case TargetType.Monster: - if (c.IsHuman || c.IsPet) { continue; } - break; + if (!Target.HasFlag(TargetType.Human)) { continue; } + } + else if (!c.IsPet) + { + if (!Target.HasFlag(TargetType.Monster)) { continue; } } //do a rough check based on the position of the character's collider first diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs index 5854fccb9..e60825beb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -4,6 +4,7 @@ using FarseerPhysics.Dynamics.Contacts; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; namespace Barotrauma.Items.Components @@ -12,6 +13,46 @@ namespace Barotrauma.Items.Components { [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "The maximum amount of force applied to the triggering entitites.", alwaysUseInstanceValues: true)] public float Force { get; set; } + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Determines if the force gets higher the closer the triggerer is to the center of the trigger.", alwaysUseInstanceValues: true)] + public bool DistanceBasedForce { get; set; } + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Determines if the force fluctuates over time or if it stays constant.", alwaysUseInstanceValues: true)] + public bool ForceFluctuation { get; set; } + [Serialize(1.0f, IsPropertySaveable.Yes, description: "How much the fluctuation affects the force. 1 is the maximum fluctuation, 0 is no fluctuation.", alwaysUseInstanceValues: true)] + private float ForceFluctuationStrength + { + get + { + return forceFluctuationStrength; + } + set + { + forceFluctuationStrength = Math.Clamp(value, 0.0f, 1.0f); + } + } + [Serialize(1.0f, IsPropertySaveable.Yes, description: "How fast (cycles per second) the force fluctuates.", alwaysUseInstanceValues: true)] + private float ForceFluctuationFrequency + { + get + { + return forceFluctuationFrequency; + } + set + { + forceFluctuationFrequency = Math.Max(value, 0.01f); + } + } + [Serialize(0.01f, IsPropertySaveable.Yes, description: "How often (in seconds) the force fluctuation is calculated.", alwaysUseInstanceValues: true)] + private float ForceFluctuationInterval + { + get + { + return forceFluctuationInterval; + } + set + { + forceFluctuationInterval = Math.Max(value, 0.01f); + } + } public PhysicsBody PhysicsBody { get; private set; } private float Radius { get; set; } @@ -38,11 +79,6 @@ namespace Barotrauma.Items.Components private readonly LevelTrigger.TriggererType triggeredBy; private readonly HashSet triggerers = new HashSet(); private readonly bool triggerOnce; - private readonly bool distanceBasedForce; - private readonly bool forceFluctuation; - private readonly float forceFluctuationStrength; - private readonly float forceFluctuationFrequency; - private readonly float forceFluctuationInterval; private readonly List statusEffectTargets = new List(); /// /// Effects applied to entities inside the trigger @@ -53,6 +89,10 @@ namespace Barotrauma.Items.Components /// private readonly List attacks = new List(); + private float forceFluctuationStrength; + private float forceFluctuationFrequency; + private float forceFluctuationInterval; + public TriggerComponent(Item item, ContentXElement element) : base(item, element) { string triggeredByAttribute = element.GetAttributeString("triggeredby", "Character"); @@ -61,15 +101,6 @@ namespace Barotrauma.Items.Components DebugConsole.ThrowError($"Error in ForceComponent config: \"{triggeredByAttribute}\" is not a valid triggerer type."); } triggerOnce = element.GetAttributeBool("triggeronce", false); - distanceBasedForce = element.GetAttributeBool("distancebasedforce", false); - forceFluctuation = element.GetAttributeBool("forcefluctuation", false); - forceFluctuationStrength = element.GetAttributeFloat("forcefluctuationstrength", 1.0f); - forceFluctuationStrength = Math.Clamp(forceFluctuationStrength, 0.0f, 1.0f); - forceFluctuationFrequency = element.GetAttributeFloat("fluctuationfrequency", 1.0f); - forceFluctuationFrequency = Math.Max(forceFluctuationFrequency, 0.01f); - forceFluctuationInterval = element.GetAttributeFloat("fluctuationinterval", 0.01f); - forceFluctuationInterval = Math.Max(forceFluctuationInterval, 0.01f); - string parentDebugName = $"TriggerComponent in {item.Name}"; foreach (var subElement in element.Elements()) { @@ -153,14 +184,14 @@ namespace Barotrauma.Items.Components TriggerActive = triggerers.Any(); - if (forceFluctuation && TriggerActive && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) + if (ForceFluctuation && TriggerActive && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) { ForceFluctuationTimer += deltaTime; - if (ForceFluctuationTimer >= forceFluctuationInterval) + if (ForceFluctuationTimer >= ForceFluctuationInterval) { - float v = MathF.Sin(2 * MathF.PI * forceFluctuationFrequency * TimeInLevel); + float v = MathF.Sin(2 * MathF.PI * ForceFluctuationFrequency * TimeInLevel); float amount = MathUtils.InverseLerp(-1.0f, 1.0f, v); - CurrentForceFluctuation = MathHelper.Lerp(1.0f - forceFluctuationStrength, 1.0f, amount); + CurrentForceFluctuation = MathHelper.Lerp(1.0f - ForceFluctuationStrength, 1.0f, amount); ForceFluctuationTimer = 0.0f; GameMain.NetworkMember?.CreateEntityEvent(this); } @@ -179,7 +210,7 @@ namespace Barotrauma.Items.Components LevelTrigger.ApplyAttacks(attacks, item.WorldPosition, deltaTime); } - if (Force < 0.01f) + if (Math.Abs(Force) < 0.01f) { // Just ignore very minimal forces continue; @@ -205,7 +236,7 @@ namespace Barotrauma.Items.Components { Vector2 diff = ConvertUnits.ToDisplayUnits(PhysicsBody.SimPosition - body.SimPosition); if (diff.LengthSquared() < 0.0001f) { return; } - float distanceFactor = distanceBasedForce ? LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits) : 1.0f; + float distanceFactor = DistanceBasedForce ? LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits) : 1.0f; if (distanceFactor <= 0.0f) { return; } Vector2 force = distanceFactor * (CurrentForceFluctuation * Force) * Vector2.Normalize(diff); if (force.LengthSquared() < 0.01f) { return; } @@ -227,5 +258,43 @@ namespace Barotrauma.Items.Components PhysicsBody.Submarine = item.Submarine; } } + + public override void ReceiveSignal(Signal signal, Connection connection) + { + base.ReceiveSignal(signal, connection); + switch (connection.Name) + { + case "set_force": + if (!FloatTryParse(signal, out float force)) { break; } + Force = force; + break; + case "set_distancebasedforce": + if (!bool.TryParse(signal.value, out bool distanceBasedForce)) { break; } + DistanceBasedForce = distanceBasedForce; + break; + case "set_forcefluctuation": + if (!bool.TryParse(signal.value, out bool forceFluctuation)) { break; } + ForceFluctuation = forceFluctuation; + break; + case "set_forcefluctuationstrength": + if (!FloatTryParse(signal, out float forceFluctuationStrength)) { break; } + ForceFluctuationStrength = forceFluctuationStrength; + break; + case "set_forcefluctuationfrequency": + if (!FloatTryParse(signal, out float forceFluctuationFrequency)) { break; } + ForceFluctuationFrequency = forceFluctuationFrequency; + break; + case "set_forcefluctuationinterval": + if (!FloatTryParse(signal, out float forceFluctuationInterval)) { break; } + ForceFluctuationInterval = forceFluctuationInterval; + break; + } + + static bool FloatTryParse(Signal signal, out float value) + { + return float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out value); + } + } } -} \ No newline at end of file +} + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 32750626f..cf6c5cc42 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -4,9 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Globalization; -using Barotrauma.IO; using System.Linq; -using System.Xml.Linq; using Barotrauma.Extensions; using FarseerPhysics.Dynamics; @@ -20,8 +18,6 @@ namespace Barotrauma.Items.Components private Vector2 barrelPos; private Vector2 transformedBarrelPos; - - private LightComponent lightComponent; private float rotation, targetRotation; @@ -71,6 +67,8 @@ namespace Barotrauma.Items.Components public Character ActiveUser; private float resetActiveUserTimer; + private List lightComponents; + public float Rotation { get { return rotation; } @@ -168,10 +166,13 @@ namespace Barotrauma.Items.Components rotation = (minRotation + maxRotation) / 2; #if CLIENT - if (lightComponent != null) + if (lightComponents != null) { - lightComponent.Rotation = rotation; - lightComponent.Light.Rotation = -rotation; + foreach (var light in lightComponents) + { + light.Rotation = rotation; + light.Light.Rotation = -rotation; + } } #endif } @@ -331,27 +332,39 @@ namespace Barotrauma.Items.Components if (loadedRotationLimits.HasValue) { RotationLimits = loadedRotationLimits.Value; } if (loadedBaseRotation.HasValue) { BaseRotation = loadedBaseRotation.Value; } targetRotation = rotation; - FindLightComponent(); UpdateTransformedBarrelPos(); } - private void FindLightComponent() + private void FindLightComponents() { + if (lightComponents != null) + { + // Can't run again, because of reparenting. + return; + } foreach (LightComponent lc in item.GetComponents()) { + // Only make the Turret control the LightComponents that are it's children. So it'd be possible to for example have some extra lights on the turret that don't rotate with it. if (lc?.Parent == this) { - lightComponent = lc; - break; + if (lightComponents == null) + { + lightComponents = new List(); + } + lightComponents.Add(lc); } } #if CLIENT - if (lightComponent != null) + if (lightComponents != null) { - lightComponent.Parent = null; - lightComponent.Rotation = Rotation - item.RotationRad; - lightComponent.Light.Rotation = -rotation; + foreach (var light in lightComponents) + { + // We want the turret to control the state of the LightComponent, not tie it's state to the state of the Turret (the light can be inactive even if the turret is active) + light.Parent = null; + light.Rotation = Rotation - item.RotationRad; + light.Light.Rotation = -rotation; + } } #endif } @@ -428,7 +441,7 @@ namespace Barotrauma.Items.Components if (MathUtils.NearlyEqual(minRotation, maxRotation)) { - UpdateLightComponent(); + UpdateLightComponents(); return; } @@ -452,7 +465,7 @@ 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.IsLoadedOutpost)) + 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)); @@ -509,14 +522,17 @@ namespace Barotrauma.Items.Components aiFindTargetTimer -= deltaTime; } - UpdateLightComponent(); + UpdateLightComponents(); } - private void UpdateLightComponent() + private void UpdateLightComponents() { - if (lightComponent != null) + if (lightComponents != null) { - lightComponent.Rotation = Rotation - item.RotationRad; + foreach (var light in lightComponents) + { + light.Rotation = Rotation - item.RotationRad; + } } } @@ -1601,21 +1617,24 @@ namespace Barotrauma.Items.Components } break; case "toggle_light": - if (lightComponent != null && signal.value != "0") + if (lightComponents != null && signal.value != "0") { - lightComponent.IsOn = !lightComponent.IsOn; - UpdateLightComponent(); + foreach (var light in lightComponents) + { + light.IsOn = !light.IsOn; + } + UpdateLightComponents(); } break; case "set_light": - if (lightComponent != null) + if (lightComponents != null) { bool shouldBeOn = signal.value != "0"; - if (shouldBeOn != lightComponent.IsOn) + foreach (var light in lightComponents) { - lightComponent.IsOn = shouldBeOn; - UpdateLightComponent(); + light.IsOn = shouldBeOn; } + UpdateLightComponents(); } break; } @@ -1633,7 +1652,7 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { base.OnItemLoaded(); - FindLightComponent(); + FindLightComponents(); targetRotation = rotation; if (!loadedBaseRotation.HasValue) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index b783a1126..ee23c7f90 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Xml.Linq; using Barotrauma.Items.Components; using Barotrauma.Networking; -using System.Collections.Immutable; using Barotrauma.Abilities; namespace Barotrauma @@ -82,7 +81,20 @@ namespace Barotrauma public string Sound { get; private set; } public Point? SheetIndex { get; private set; } - public LightComponent LightComponent { get; set; } + public LightComponent LightComponent => LightComponents?.FirstOrDefault(); + + public List LightComponents + { + get + { + if (_lightComponents == null) + { + _lightComponents = new List(); + } + return _lightComponents; + } + } + private List _lightComponents; public int Variant { get; set; } @@ -338,11 +350,14 @@ namespace Barotrauma.Items.Components foreach (var lightElement in subElement.Elements()) { if (!lightElement.Name.ToString().Equals("lightcomponent", StringComparison.OrdinalIgnoreCase)) { continue; } - wearableSprites[i].LightComponent = new LightComponent(item, lightElement) + wearableSprites[i].LightComponents.Add(new LightComponent(item, lightElement) { Parent = this - }; - item.AddComponent(wearableSprites[i].LightComponent); + }); + foreach (var light in wearableSprites[i].LightComponents) + { + item.AddComponent(light); + } } i++; @@ -413,7 +428,10 @@ namespace Barotrauma.Items.Components IsActive = true; if (wearableSprite.LightComponent != null) { - wearableSprite.LightComponent.ParentBody = equipLimb.body; + foreach (var light in wearableSprite.LightComponents) + { + light.ParentBody = equipLimb.body; + } } limb[i] = equipLimb; @@ -467,7 +485,10 @@ namespace Barotrauma.Items.Components if (wearableSprites[i].LightComponent != null) { - wearableSprites[i].LightComponent.ParentBody = null; + foreach (var light in wearableSprites[i].LightComponents) + { + light.ParentBody = null; + } } equipLimb.WearingItems.RemoveAll(w => w != null && w == wearableSprites[i]); @@ -494,7 +515,6 @@ namespace Barotrauma.Items.Components } item.SetTransform(picker.SimPosition, 0.0f); - item.SetContainedItemPositions(); item.ApplyStatusEffects(ActionType.OnWearing, deltaTime, picker); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 8f7b0e4fd..c7c0b2425 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -24,13 +24,18 @@ namespace Barotrauma partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerPositionSync, IClientSerializable { public static List ItemList = new List(); + + private static readonly HashSet dangerousItems = new HashSet(); + + public static IReadOnlyCollection DangerousItems { get { return dangerousItems; } } + public new ItemPrefab Prefab => base.Prefab as ItemPrefab; public static bool ShowLinks = true; private readonly HashSet tags; - private bool isWire, isLogic; + private readonly bool isWire, isLogic; private Hull currentHull; public Hull CurrentHull @@ -279,7 +284,10 @@ namespace Barotrauma if (Screen.Selected == GameMain.SubEditorScreen) { SetContainedItemPositions(); - GetComponent()?.SetLightSourceTransform(); + foreach (var light in GetComponents()) + { + light.SetLightSourceTransform(); + } } #endif } @@ -1001,6 +1009,10 @@ namespace Barotrauma InsertToList(); ItemList.Add(this); + if (Prefab.IsDangerous) + { + dangerousItems.Add(this); + } DebugConsole.Log("Created " + Name + " (" + ID + ")"); @@ -1092,17 +1104,16 @@ namespace Barotrauma component.OnActiveStateChanged += (bool isActive) => { - bool hasSounds = false; + bool needsSoundUpdate = false; #if CLIENT - hasSounds = component.HasSounds; + needsSoundUpdate = component.NeedsSoundUpdate(); #endif //component doesn't need to be updated if it isn't active, doesn't have a parent that could activate it, - //nor status effects, sounds or conditionals that would need to run + //nor sounds or conditionals that would need to run if (!isActive && !component.UpdateWhenInactive && - !hasSounds && + !needsSoundUpdate && component.Parent == null && - (component.IsActiveConditionals == null || !component.IsActiveConditionals.Any()) && - (component.statusEffectLists == null || !component.statusEffectLists.Any())) + (component.IsActiveConditionals == null || !component.IsActiveConditionals.Any())) { if (updateableComponents.Contains(component)) { updateableComponents.Remove(component); } } @@ -1499,6 +1510,11 @@ namespace Barotrauma public void ApplyStatusEffect(StatusEffect effect, ActionType type, float deltaTime, Character character = null, Limb limb = null, Entity useTarget = null, bool isNetworkEvent = false, bool checkCondition = true, Vector2? worldPosition = null) { + if (effect.intervalTimer > 0.0f) + { + effect.intervalTimer -= deltaTime; + return; + } if (!isNetworkEvent && checkCondition) { if (condition == 0.0f && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { return; } @@ -1630,6 +1646,7 @@ namespace Barotrauma foreach (ItemComponent ic in components) { ic.PlaySound(ActionType.OnBroken); + ic.StopSounds(ActionType.OnActive); } if (Screen.Selected == GameMain.SubEditorScreen) { return; } #endif @@ -1722,6 +1739,19 @@ namespace Barotrauma public override void Update(float deltaTime, Camera cam) { +#if SERVER + if (!(Submarine is { Loading: true })) + { + sendConditionUpdateTimer -= deltaTime; + if (conditionUpdatePending && sendConditionUpdateTimer <= 0.0f) + { + SendPendingNetworkUpdates(); + } + } +#endif + + if (!isActive) { return; } + if (impactQueue != null) { while (impactQueue.TryDequeue(out float impact)) @@ -1730,22 +1760,11 @@ namespace Barotrauma } } - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && (!Submarine?.Loading ?? true)) - { - sendConditionUpdateTimer -= deltaTime; - if (conditionUpdatePending && sendConditionUpdateTimer <= 0.0f) - { - SendPendingNetworkUpdates(); - } - } - - if (aiTarget != null) + if (aiTarget != null && aiTarget.NeedsUpdate) { aiTarget.Update(deltaTime); } - if (!isActive) { return; } - ApplyStatusEffects(ActionType.Always, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character); ApplyStatusEffects(parentInventory == null ? ActionType.OnNotContained : ActionType.OnContained, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character); @@ -1846,7 +1865,10 @@ namespace Barotrauma } else { - if (updateableComponents.Count == 0 && !hasStatusEffectsOfType[(int)ActionType.Always] && (body == null || !body.Enabled)) + if (updateableComponents.Count == 0 && + (aiTarget == null || !aiTarget.NeedsUpdate) && + !hasStatusEffectsOfType[(int)ActionType.Always] && + (body == null || !body.Enabled)) { #if CLIENT positionBuffer.Clear(); @@ -1983,6 +2005,7 @@ namespace Barotrauma impactQueue ??= new ConcurrentQueue(); impactQueue.Enqueue(impact); + isActive = true; return true; } @@ -2501,11 +2524,9 @@ namespace Barotrauma if (ic.Use(deltaTime, character)) { ic.WasUsed = true; - #if CLIENT - ic.PlaySound(ActionType.OnUse, character); -#endif - + ic.PlaySound(ActionType.OnUse, character); +#endif ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb); if (ic.DeleteOnUse) { remove = true; } @@ -2534,11 +2555,9 @@ namespace Barotrauma if (ic.SecondaryUse(deltaTime, character)) { ic.WasSecondaryUsed = true; - #if CLIENT ic.PlaySound(ActionType.OnSecondaryUse, character); #endif - ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character); if (ic.DeleteOnUse) { remove = true; } @@ -3219,9 +3238,17 @@ namespace Barotrauma item.RecalculateConditionValues(); item.SetActiveSprite(); - if (submarine?.Info.GameVersion != null) + Version savedVersion = submarine?.Info.GameVersion; + if (element.Document?.Root != null && element.Document.Root.Name.ToString().Equals("gamesession", StringComparison.OrdinalIgnoreCase)) { - SerializableProperty.UpgradeGameVersion(item, item.Prefab.ConfigElement, submarine.Info.GameVersion); + //character inventories are loaded from the game session file - use the version number of the saved game session instead of the sub + //(the sub may have already been saved and up-to-date, even though the character inventories aren't) + savedVersion = new Version(element.Document.Root.GetAttributeString("version", "0.0.0.0")); + } + + if (savedVersion != null) + { + SerializableProperty.UpgradeGameVersion(item, item.Prefab.ConfigElement, savedVersion); } foreach (ItemComponent component in item.components) @@ -3342,6 +3369,7 @@ namespace Barotrauma ic.ShallowRemove(); } ItemList.Remove(this); + dangerousItems.Remove(this); if (body != null) { @@ -3400,6 +3428,7 @@ namespace Barotrauma #endif } ItemList.Remove(this); + dangerousItems.Remove(this); if (body != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/StartItemSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/StartItemSet.cs index a93ea13c3..a8c23fb81 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/StartItemSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/StartItemSet.cs @@ -26,9 +26,15 @@ namespace Barotrauma public readonly ImmutableArray Items; + /// + /// The order in which the sets are displayed in menus + /// + public readonly int Order; + public StartItemSet(ContentXElement element, StartItemsFile file) : base(file, element.GetAttributeIdentifier("identifier", Identifier.Empty)) { Items = element.Elements().Select(e => new StartItem(e!)).ToImmutableArray(); + Order = element.GetAttributeInt("order", 0); } public override void Dispose() { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 227f59f3d..574115e1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -10,12 +10,11 @@ using System.Net; namespace Barotrauma { + #warning TODO: MapEntityPrefab should be constrained further to not include item assemblies, as assemblies are effectively not entities at all partial class ItemAssemblyPrefab : MapEntityPrefab { public static readonly PrefabCollection Prefabs = new PrefabCollection(); - public static readonly string VanillaSaveFolder = Path.Combine("Content", "Items", "Assemblies"); - private readonly XElement configElement; public readonly ImmutableArray<(Identifier Identifier, Rectangle Rect)> DisplayEntities; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 61049099d..d6be0f566 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -49,7 +49,7 @@ namespace Barotrauma Cave = 0x4, Ruin = 0x8, Wreck = 0x10, - BeaconStation = 0x20, // Not used anywhere + BeaconStation = 0x20, Abyss = 0x40, AbyssCave = 0x80 } @@ -395,6 +395,13 @@ namespace Barotrauma /// public static bool IsLoadedOutpost => Loaded?.Type == LevelData.LevelType.Outpost; + /// + /// Is there a loaded level set, and is it a friendly outpost (FriendlyNPC or Team1) + /// + public static bool IsLoadedFriendlyOutpost => + loaded?.Type == LevelData.LevelType.Outpost && + (loaded?.StartLocation?.Type?.OutpostTeam == CharacterTeamType.FriendlyNPC || loaded?.StartLocation?.Type?.OutpostTeam == CharacterTeamType.Team1); + public LevelGenerationParams GenerationParams { get { return LevelData.GenerationParams; } @@ -421,7 +428,7 @@ namespace Barotrauma borders = new Rectangle(Point.Zero, levelData.Size); } - public static Level Generate(LevelData levelData, bool mirror, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null) + public static Level Generate(LevelData levelData, bool mirror, Location startLocation, Location endLocation, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null) { Debug.Assert(levelData.Biome != null); if (levelData.Biome == null) { throw new ArgumentException("Biome was null"); } @@ -433,11 +440,11 @@ namespace Barotrauma preSelectedStartOutpost = startOutpost, preSelectedEndOutpost = endOutpost }; - level.Generate(mirror); + level.Generate(mirror, startLocation, endLocation); return level; } - private void Generate(bool mirror) + private void Generate(bool mirror, Location startLocation, Location endLocation) { Loaded?.Remove(); Loaded = this; @@ -454,8 +461,8 @@ namespace Barotrauma if (LevelData.ForceOutpostGenerationParams == null) { - StartLocation = GameMain.GameSession?.StartLocation; - EndLocation = GameMain.GameSession?.EndLocation; + StartLocation = startLocation; + EndLocation = endLocation; } GenerateEqualityCheckValue(LevelGenStage.GenStart); @@ -509,7 +516,7 @@ namespace Barotrauma Rectangle pathBorders = borders; pathBorders.Inflate( -Math.Min(Math.Min(minMainPathWidth * 2, MaxSubmarineWidth), borders.Width / 5), - -Math.Min(minMainPathWidth, borders.Height / 5)); + -Math.Min(minMainPathWidth * 2, borders.Height / 5)); if (pathBorders.Width <= 0) { throw new InvalidOperationException($"The width of the level's path area is invalid ({pathBorders.Width})"); } if (pathBorders.Height <= 0) { throw new InvalidOperationException($"The height of the level's path area is invalid ({pathBorders.Height})"); } @@ -1713,7 +1720,7 @@ namespace Barotrauma #endif } } - else + else if (abyssHeight > 30000) { //if the bottom of the abyss area is below crush depth, try to move it up to keep (most) of the abyss content above crush depth //but only if start of the abyss is above crush depth (no point in doing this if all of it is below crush depth) @@ -3527,6 +3534,8 @@ namespace Barotrauma } else if (type == SubmarineType.BeaconStation) { + PositionsOfInterest.Add(new InterestingPosition(spawnPoint.ToPoint(), PositionType.BeaconStation, submarine: sub)); + sub.ShowSonarMarker = false; sub.DockedTo.ForEach(s => s.ShowSonarMarker = false); sub.PhysicsBody.FarseerBody.BodyType = BodyType.Static; @@ -3940,7 +3949,7 @@ namespace Barotrauma //the submarine port has to be at the top of the sub if (port.Item.WorldPosition.Y < Submarine.MainSub.WorldPosition.Y) { continue; } float dist = Math.Abs(port.Item.WorldPosition.X - Submarine.MainSub.WorldPosition.X); - if (dist < closestDistance) + if (dist < closestDistance || subPort.MainDockingPort) { subPort = port; closestDistance = dist; @@ -4023,6 +4032,26 @@ namespace Barotrauma DebugConsole.ThrowError("No BeaconStation files found in the selected content packages!"); return; } + + var beaconInfos = SubmarineInfo.SavedSubmarines.Where(i => i.IsBeacon); + for (int i = beaconStationFiles.Count - 1; i >= 0; i--) + { + var beaconStationFile = beaconStationFiles[i]; + var matchingInfo = beaconInfos.SingleOrDefault(info => info.FilePath == beaconStationFile.Path.Value); + Debug.Assert(matchingInfo != null); + if (matchingInfo?.BeaconStationInfo is BeaconStationInfo beaconInfo) + { + if (LevelData.Difficulty < beaconInfo.MinLevelDifficulty || LevelData.Difficulty > beaconInfo.MaxLevelDifficulty) + { + beaconStationFiles.RemoveAt(i); + } + } + } + if (beaconStationFiles.None()) + { + DebugConsole.ThrowError($"No BeaconStation files found for the level difficulty {LevelData.Difficulty}!"); + return; + } var contentFile = beaconStationFiles.GetRandom(Rand.RandSync.ServerAndClient); string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value); @@ -4078,24 +4107,22 @@ namespace Barotrauma { if (!(GameMain.NetworkMember?.IsClient ?? false)) { - //empty the reactor - if (reactorContainer != null) + bool allowDisconnectedWires = true; + bool allowDamagedWalls = true; + if (BeaconStation.Info?.BeaconStationInfo is BeaconStationInfo info) { - foreach (Item item in reactorContainer.Inventory.AllItems) - { - if (item.NonInteractable) { continue; } - Spawner.AddItemToRemoveQueue(item); - } + allowDisconnectedWires = info.AllowDisconnectedWires; + allowDamagedWalls = info.AllowDamagedWalls; } //remove wires float removeWireMinDifficulty = 20.0f; float removeWireProbability = MathUtils.InverseLerp(removeWireMinDifficulty, 100.0f, LevelData.Difficulty) * 0.5f; - if (removeWireProbability > 0.0f) + if (removeWireProbability > 0.0f && allowDisconnectedWires) { foreach (Item item in beaconItems.Where(it => it.GetComponent() != null).ToList()) { - if (item.NonInteractable) { continue; } + if (item.NonInteractable || item.InvulnerableToDamage) { continue; } Wire wire = item.GetComponent(); if (wire.Locked) { continue; } if (wire.Connections[0] != null && (wire.Connections[0].Item.NonInteractable || wire.Connections[0].Item.GetComponent().Locked)) @@ -4115,8 +4142,8 @@ namespace Barotrauma connection.ConnectionPanel.DisconnectedWires.Add(wire); wire.RemoveConnection(connection.Item); #if SERVER - connection.ConnectionPanel.Item.CreateServerEvent(connection.ConnectionPanel); - wire.CreateNetworkEvent(); + connection.ConnectionPanel.Item.CreateServerEvent(connection.ConnectionPanel); + wire.CreateNetworkEvent(); #endif } } @@ -4124,23 +4151,25 @@ namespace Barotrauma } } - //break powered items - foreach (Item item in beaconItems.Where(it => it.Components.Any(c => c is Powered) && it.Components.Any(c => c is Repairable))) + if (allowDamagedWalls) { - if (item.NonInteractable) { continue; } - if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.5f) + //break powered items + foreach (Item item in beaconItems.Where(it => it.Components.Any(c => c is Powered) && it.Components.Any(c => c is Repairable))) { - item.Condition *= Rand.Range(0.6f, 0.8f, Rand.RandSync.Unsynced); + if (item.NonInteractable || item.InvulnerableToDamage) { continue; } + if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.5f) + { + item.Condition *= Rand.Range(0.6f, 0.8f, Rand.RandSync.Unsynced); + } } - } - - //poke holes in the walls - foreach (Structure structure in Structure.WallList.Where(s => s.Submarine == BeaconStation)) - { - if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.25f) + //poke holes in the walls + foreach (Structure structure in Structure.WallList.Where(s => s.Submarine == BeaconStation)) { - int sectionIndex = Rand.Range(0, structure.SectionCount - 1, Rand.RandSync.Unsynced); - structure.AddDamage(sectionIndex, Rand.Range(structure.MaxHealth * 0.2f, structure.MaxHealth, Rand.RandSync.Unsynced)); + if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.25f) + { + int sectionIndex = Rand.Range(0, structure.SectionCount - 1, Rand.RandSync.Unsynced); + structure.AddDamage(sectionIndex, Rand.Range(structure.MaxHealth * 0.2f, structure.MaxHealth, Rand.RandSync.Unsynced)); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 10f625e3b..a6de99b67 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -20,7 +20,7 @@ namespace Barotrauma public readonly string Seed; - public float Difficulty; + public readonly float Difficulty; public readonly Biome Biome; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 0ace84cde..9e550b39b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -432,7 +432,7 @@ namespace Barotrauma if (sub != null) { bool leaveBehind = false; - if (!sub.DockedTo.Contains(Submarine.MainSub)) + if (sub.Submarine != null && !sub.DockedTo.Contains(sub.Submarine)) { System.Diagnostics.Debug.Assert(Submarine.MainSub.AtEndExit || Submarine.MainSub.AtStartExit); if (Submarine.MainSub.AtEndExit) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index af68c1464..274935593 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -78,7 +78,7 @@ namespace Barotrauma /// /// Load a previously saved campaign map from XML /// - private Map(CampaignMode campaign, XElement element, CampaignSettings settings) : this(settings) + private Map(CampaignMode campaign, XElement element) : this(campaign.Settings) { Seed = element.GetAttributeString("seed", "a"); Rand.SetSyncedSeed(ToolBox.StringToInt(Seed)); @@ -104,7 +104,7 @@ namespace Barotrauma case "radiation": Radiation = new Radiation(this, generationParams.RadiationParams, subElement) { - Enabled = settings.RadiationEnabled + Enabled = campaign.Settings.RadiationEnabled }; break; } @@ -208,12 +208,12 @@ namespace Barotrauma /// /// Generate a new campaign map from the seed /// - public Map(CampaignMode campaign, string seed, CampaignSettings settings) : this(settings) + public Map(CampaignMode campaign, string seed) : this(campaign.Settings) { Seed = seed; Rand.SetSyncedSeed(ToolBox.StringToInt(Seed)); - Generate(); + Generate(campaign.Settings); if (Locations.Count == 0) { @@ -228,10 +228,7 @@ namespace Barotrauma foreach (Location location in Locations) { if (location.Type.Identifier != "outpost") { continue; } - if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X) - { - CurrentLocation = StartLocation = furthestDiscoveredLocation = location; - } + SetStartLocation(location); } //if no outpost was found (using a mod that replaces the outpost location type?), find any type of outpost if (CurrentLocation == null) @@ -239,25 +236,36 @@ namespace Barotrauma foreach (Location location in Locations) { if (!location.Type.HasOutpost) { continue; } - if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X) - { - CurrentLocation = StartLocation = furthestDiscoveredLocation = location; - } + SetStartLocation(location); } } - System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation."); - if (StartLocation?.LevelData != null) + + void SetStartLocation(Location location) { - StartLocation.LevelData.Difficulty = 0; + if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X) + { + CurrentLocation = StartLocation = furthestDiscoveredLocation = location; + } } - //ensure all paths from the starting location have 0 difficulty to make the 1st campaign round very easy - foreach (var locationConnection in StartLocation.Connections) + 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 (locationConnection.Difficulty > 0.0f) + if (StartLocation != null) { - locationConnection.Difficulty = 0.0f; - locationConnection.LevelData = new LevelData(locationConnection); + StartLocation.LevelData = new LevelData(StartLocation, 0); + } + + //ensure all paths from the starting location have 0 difficulty to make the 1st campaign round very easy + foreach (var locationConnection in StartLocation.Connections) + { + if (locationConnection.Difficulty > 0.0f) + { + locationConnection.Difficulty = 0.0f; + locationConnection.LevelData = new LevelData(locationConnection); + } } } @@ -276,7 +284,7 @@ namespace Barotrauma #region Generation - private void Generate() + private void Generate(CampaignSettings settings) { Connections.Clear(); Locations.Clear(); @@ -294,7 +302,6 @@ namespace Barotrauma Voronoi voronoi = new Voronoi(0.5f); List edges = voronoi.MakeVoronoiGraph(voronoiSites, Width, Height); - float zoneWidth = Width / generationParams.DifficultyZones; Vector2 margin = new Vector2( Math.Min(10, Width * 0.1f), @@ -310,6 +317,7 @@ namespace Barotrauma voronoiSites.Clear(); Dictionary> locationsPerZone = new Dictionary>(); + bool possibleStartOutpostCreated = false; foreach (GraphEdge edge in edges) { if (edge.Point1 == edge.Point2) { continue; } @@ -344,12 +352,26 @@ namespace Barotrauma } LocationType forceLocationType = null; - foreach (LocationType locationType in LocationType.Prefabs.OrderBy(lt => lt.Identifier)) + if (!possibleStartOutpostCreated) { - if (locationType.MinCountPerZone.TryGetValue(zone, out int minCount) && locationsPerZone[zone].Count(l => l.Type == locationType) < minCount) + float zoneWidth = Width / generationParams.DifficultyZones; + float threshold = zoneWidth * 0.1f; + if (position.X < threshold) { - forceLocationType = locationType; - break; + LocationType.Prefabs.TryGet("outpost", out forceLocationType); + possibleStartOutpostCreated = true; + } + } + + if (forceLocationType == null) + { + foreach (LocationType locationType in LocationType.Prefabs.OrderBy(lt => lt.Identifier)) + { + if (locationType.MinCountPerZone.TryGetValue(zone, out int minCount) && locationsPerZone[zone].Count(l => l.Type == locationType) < minCount) + { + forceLocationType = locationType; + break; + } } } @@ -455,9 +477,7 @@ namespace Barotrauma if (zone1 == zone2) { continue; } if (zone1 > zone2) { - int temp = zone2; - zone2 = zone1; - zone1 = temp; + (zone1, zone2) = (zone2, zone1); } if (generationParams.GateCount[zone1] == 0) { continue; } @@ -527,32 +547,43 @@ namespace Barotrauma foreach (LocationConnection connection in Connections) { - float difficulty = connection.CenterPos.X / Width * 100; - float minDifficulty = 0; - float maxDifficulty = 100; - var biome = connection.Biome; - if (biome != null) + if (connection.Locations.Any(l => l.IsGateBetweenBiomes)) { - minDifficulty = connection.Biome.MinDifficulty; - maxDifficulty = connection.Biome.MaxDifficulty; - if (connection.Locked) - { - connection.Difficulty = maxDifficulty; - } + connection.Difficulty = connection.Locations.Min(l => l.Biome.MaxDifficulty); + } + else + { + connection.Difficulty = CalculateDifficulty(connection.CenterPos.X, connection.Biome); } - connection.Difficulty = MathHelper.Clamp(difficulty, minDifficulty, maxDifficulty); } CreateEndLocation(); foreach (Location location in Locations) { - location.LevelData = new LevelData(location, MathHelper.Clamp(location.MapPosition.X / Width * 100, 0.0f, 100.0f)); + location.LevelData = new LevelData(location, CalculateDifficulty(location.MapPosition.X, location.Biome)); } foreach (LocationConnection connection in Connections) { connection.LevelData = new LevelData(connection); } + + float CalculateDifficulty(float mapPosition, Biome biome) + { + float settingsFactor = settings.LevelDifficultyMultiplier; + float minDifficulty = 0; + float maxDifficulty = 100; + float difficulty = mapPosition / Width * 100; + System.Diagnostics.Debug.Assert(biome != null); + if (biome != null) + { + minDifficulty = biome.MinDifficulty; + maxDifficulty = biome.MaxDifficulty; + float diff = 1 - settingsFactor; + difficulty *= 1 - (1f / biome.AllowedZones.Max() * diff); + } + return MathHelper.Clamp(difficulty, minDifficulty, maxDifficulty); + } } partial void GenerateLocationConnectionVisuals(); @@ -633,6 +664,11 @@ namespace Barotrauma if (EndLocation == null || previousToEndLocation == null) { return; } + if (LocationType.Prefabs.TryGet("none", out LocationType locationType)) + { + previousToEndLocation.ChangeType(locationType); + } + //remove all locations from the end biome except the end location for (int i = Locations.Count - 1; i >= 0; i--) { @@ -652,7 +688,7 @@ namespace Barotrauma } //removed all connections from the second-to-last location, need to reconnect it - if (!previousToEndLocation.Connections.Any()) + if (previousToEndLocation.Connections.None()) { Location connectTo = Locations.First(); foreach (Location location in Locations) @@ -759,6 +795,7 @@ namespace Barotrauma CurrentLocation = Locations[index]; CurrentLocation.Discover(); + CurrentLocation.CreateStores(); if (prevLocation != CurrentLocation) { var connection = CurrentLocation.Connections.Find(c => c.Locations.Contains(prevLocation)); @@ -766,10 +803,8 @@ namespace Barotrauma { connection.Passed = true; } + OnLocationChanged?.Invoke(prevLocation, CurrentLocation); } - - CurrentLocation.CreateStores(); - OnLocationChanged?.Invoke(prevLocation, CurrentLocation); } public void SelectLocation(int index) @@ -789,6 +824,7 @@ namespace Barotrauma return; } + Location prevSelected = SelectedLocation; SelectedLocation = Locations[index]; var currentDisplayLocation = GameMain.GameSession?.Campaign?.GetCurrentDisplayLocation(); SelectedConnection = @@ -798,7 +834,10 @@ namespace Barotrauma { DebugConsole.ThrowError("A locked connection was selected - this should not be possible.\n" + Environment.StackTrace.CleanupStackTrace()); } - OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection); + if (prevSelected != SelectedLocation) + { + OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection); + } } public void SelectLocation(Location location) @@ -811,13 +850,17 @@ namespace Barotrauma return; } + Location prevSelected = SelectedLocation; SelectedLocation = location; SelectedConnection = Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation)); if (SelectedConnection?.Locked ?? false) { DebugConsole.ThrowError("A locked connection was selected - this should not be possible.\n" + Environment.StackTrace.CleanupStackTrace()); } - OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection); + if (prevSelected != SelectedLocation) + { + OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection); + } } public void SelectMission(IEnumerable missionIndices) @@ -830,23 +873,24 @@ namespace Barotrauma return; } - CurrentLocation.SetSelectedMissionIndices(missionIndices); - - foreach (Mission selectedMission in CurrentLocation.SelectedMissions.ToList()) + if (!missionIndices.SequenceEqual(GetSelectedMissionIndices())) { - if (selectedMission.Locations[0] != CurrentLocation || - selectedMission.Locations[1] != CurrentLocation) + CurrentLocation.SetSelectedMissionIndices(missionIndices); + foreach (Mission selectedMission in CurrentLocation.SelectedMissions.ToList()) { - if (SelectedConnection == null) { return; } - //the destination must be the same as the destination of the mission - if (selectedMission.Locations[1] != SelectedLocation) + if (selectedMission.Locations[0] != CurrentLocation || + selectedMission.Locations[1] != CurrentLocation) { - CurrentLocation.DeselectMission(selectedMission); + if (SelectedConnection == null) { return; } + //the destination must be the same as the destination of the mission + if (selectedMission.Locations[1] != SelectedLocation) + { + CurrentLocation.DeselectMission(selectedMission); + } } } + OnMissionsSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMissions); } - - OnMissionsSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMissions); } public void SelectRandomLocation(bool preferUndiscovered) @@ -1070,9 +1114,9 @@ namespace Barotrauma /// /// Load a previously saved map from an xml element /// - public static Map Load(CampaignMode campaign, XElement element, CampaignSettings settings) + public static Map Load(CampaignMode campaign, XElement element) { - Map map = new Map(campaign, element, settings); + Map map = new Map(campaign, element); map.LoadState(element, false); #if CLIENT map.DrawOffset = -map.CurrentLocation.MapPosition; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs new file mode 100644 index 000000000..7d9f33be6 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma +{ + class BeaconStationInfo : ISerializableEntity + { + [Serialize(true, IsPropertySaveable.Yes), Editable] + public bool AllowDamagedWalls { get; set; } + + [Serialize(true, IsPropertySaveable.Yes), Editable] + public bool AllowDisconnectedWires { get; set; } + + [Serialize(0.0f, IsPropertySaveable.Yes), Editable] + public float MinLevelDifficulty { get; set; } + + [Serialize(100.0f, IsPropertySaveable.Yes), Editable] + public float MaxLevelDifficulty { get; set; } + + public string Name { get; private set; } + + public Dictionary SerializableProperties { get; private set; } + + public BeaconStationInfo(SubmarineInfo submarineInfo, XElement element) + { + Name = $"BeaconStationInfo ({submarineInfo.Name})"; + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + } + + public BeaconStationInfo(SubmarineInfo submarineInfo) + { + Name = $"BeaconStationInfo ({submarineInfo.Name})"; + SerializableProperties = SerializableProperty.DeserializeProperties(this); + } + + public BeaconStationInfo(BeaconStationInfo original) + { + Name = original.Name; + SerializableProperties = new Dictionary(); + foreach (KeyValuePair kvp in original.SerializableProperties) + { + SerializableProperties.Add(kvp.Key, kvp.Value); + if (SerializableProperty.GetSupportedTypeName(kvp.Value.PropertyType) != null) + { + kvp.Value.TrySetValue(this, kvp.Value.GetValue(original)); + } + } + } + + public void Save(XElement element) + { + SerializableProperty.SerializeProperties(this, element); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 8e598475e..1da36a5fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -830,43 +830,44 @@ namespace Barotrauma private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable modules, Identifier moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType, bool allowDifferentLocationType) { - IEnumerable availableModules = null; + IEnumerable modulesWithCorrectFlags = null; if (moduleFlag.IsEmpty || moduleFlag.Equals("none")) { - availableModules = modules + modulesWithCorrectFlags = modules .Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier()))); } else { - availableModules = modules + modulesWithCorrectFlags = modules .Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag)); } + modulesWithCorrectFlags = modulesWithCorrectFlags.Where(m => m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition) && m.OutpostModuleInfo.CanAttachToPrevious.HasFlag(gapPosition)); - availableModules = availableModules.Where(m => m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition) && m.OutpostModuleInfo.CanAttachToPrevious.HasFlag(gapPosition)); - - if (prevModule != null) + var suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true); + if (!suitableModules.Any()) { - availableModules = availableModules.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule));// && CanAttachTo(prevModule, m.OutpostModuleInfo)); + //no suitable module found, see if we can find a "generic" module that's not meant for any specific type of outpost + suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false); + //still not found, see if we can find something that's otherwise suitable but not meant to attach to the previous module + if (!suitableModules.Any()) + { + suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true); + } + //still not found! Try if we can find a generic module that's not meant to attach to the previous module + if (!suitableModules.Any()) + { + suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false); + } } - if (availableModules.Count() == 0) { 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)); - - //if not found, search for modules suitable for any location type - if (allowDifferentLocationType && !modulesSuitableForLocationType.Any()) - { - modulesSuitableForLocationType = availableModules.Where(m => !m.OutpostModuleInfo.AllowedLocationTypes.Any()); - } - - if (!modulesSuitableForLocationType.Any()) + if (!suitableModules.Any()) { if (allowDifferentLocationType) { + if (modulesWithCorrectFlags.Any()) + DebugConsole.NewMessage($"Could not find a suitable module for the location type {locationType}. Module flag: {moduleFlag}.", Color.Orange); - return ToolBox.SelectWeightedRandom(availableModules.ToList(), availableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); + return ToolBox.SelectWeightedRandom(modulesWithCorrectFlags.ToList(), modulesWithCorrectFlags.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); } else { @@ -875,7 +876,28 @@ namespace Barotrauma } else { - return ToolBox.SelectWeightedRandom(modulesSuitableForLocationType.ToList(), modulesSuitableForLocationType.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); + return ToolBox.SelectWeightedRandom(suitableModules.ToList(), suitableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); + } + + IEnumerable GetSuitable(IEnumerable modules, bool requireAllowAttachToPrevious, bool requireCorrectLocationType, bool disallowNonLocationTypeSpecific) + { + IEnumerable suitable = modules; + if (requireCorrectLocationType) + { + if (disallowNonLocationTypeSpecific) + { + 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()); + } + } + if (requireAllowAttachToPrevious && prevModule != null) + { + suitable = suitable.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule)); + } + return suitable; } } @@ -1590,10 +1612,6 @@ namespace Barotrauma { npc.CharacterHealth.Unkillable = true; } - else - { - npc.AddStaticHealthMultiplier(humanPrefab.HealthMultiplier); - } humanPrefab.GiveItems(npc, outpost, Rand.RandSync.ServerAndClient); foreach (Item item in npc.Inventory.FindAllItems(it => it != null, recursive: true)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index b5f7d87ba..18321db98 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -1483,8 +1483,10 @@ namespace Barotrauma { if (item.Submarine != this) continue; if (item.ParentInventory != null || item.body != null) continue; - var lightComponent = item.GetComponent(); - if (lightComponent != null) lightComponent.LightColor = new Color(lightComponent.LightColor, lightComponent.LightColor.A / 255.0f * 0.5f); + foreach (var light in item.GetComponents()) + { + light.LightColor = new Color(light.LightColor, light.LightColor.A / 255.0f * 0.5f); + } } } GenerateOutdoorNodes(); @@ -1555,7 +1557,7 @@ namespace Barotrauma element.Add(new XAttribute("cargocapacity", cargoCapacity)); element.Add(new XAttribute("recommendedcrewsizemin", Info.RecommendedCrewSizeMin)); element.Add(new XAttribute("recommendedcrewsizemax", Info.RecommendedCrewSizeMax)); - element.Add(new XAttribute("recommendedcrewexperience", Info.RecommendedCrewExperience ?? "")); + element.Add(new XAttribute("recommendedcrewexperience", Info.RecommendedCrewExperience.ToString())); element.Add(new XAttribute("requiredcontentpackages", string.Join(", ", Info.RequiredContentPackages))); if (Info.Type == SubmarineType.OutpostModule) @@ -1632,6 +1634,7 @@ namespace Barotrauma Type = Info.Type, FilePath = filePath, OutpostModuleInfo = Info.OutpostModuleInfo != null ? new OutpostModuleInfo(Info.OutpostModuleInfo) : null, + BeaconStationInfo = Info.BeaconStationInfo != null ? new BeaconStationInfo(Info.BeaconStationInfo) : null, Name = Path.GetFileNameWithoutExtension(filePath) }; #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index fb29d2652..3d1ca403e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -39,7 +39,15 @@ namespace Barotrauma public SubmarineTag Tags { get; private set; } public int RecommendedCrewSizeMin = 1, RecommendedCrewSizeMax = 2; - public string RecommendedCrewExperience; + + public enum CrewExperienceLevel + { + Unknown, + CrewExperienceLow, + CrewExperienceMid, + CrewExperienceHigh + } + public CrewExperienceLevel RecommendedCrewExperience; /// /// A random int that gets assigned when saving the sub. Used in mp campaign to verify that sub files match @@ -89,6 +97,7 @@ namespace Barotrauma public SubmarineClass SubmarineClass; public OutpostModuleInfo OutpostModuleInfo { get; set; } + public BeaconStationInfo BeaconStationInfo { get; set; } public bool IsOutpost => Type == SubmarineType.Outpost || Type == SubmarineType.OutpostModule; @@ -280,6 +289,10 @@ namespace Barotrauma { OutpostModuleInfo = new OutpostModuleInfo(original.OutpostModuleInfo); } + if (original.BeaconStationInfo != null) + { + BeaconStationInfo = new BeaconStationInfo(original.BeaconStationInfo); + } #if CLIENT PreviewImage = original.PreviewImage != null ? new Sprite(original.PreviewImage) : null; #endif @@ -330,7 +343,24 @@ namespace Barotrauma CargoCapacity = SubmarineElement.GetAttributeInt("cargocapacity", -1); RecommendedCrewSizeMin = SubmarineElement.GetAttributeInt("recommendedcrewsizemin", 0); RecommendedCrewSizeMax = SubmarineElement.GetAttributeInt("recommendedcrewsizemax", 0); - RecommendedCrewExperience = SubmarineElement.GetAttributeString("recommendedcrewexperience", "Unknown"); + var recommendedCrewExperience = SubmarineElement.GetAttributeIdentifier("recommendedcrewexperience", CrewExperienceLevel.Unknown.ToIdentifier()); + // Backwards compatibility + if (recommendedCrewExperience == "Beginner") + { + RecommendedCrewExperience = CrewExperienceLevel.CrewExperienceLow; + } + else if (recommendedCrewExperience == "Intermediate") + { + RecommendedCrewExperience = CrewExperienceLevel.CrewExperienceMid; + } + else if (recommendedCrewExperience == "Experienced") + { + RecommendedCrewExperience = CrewExperienceLevel.CrewExperienceHigh; + } + else + { + Enum.TryParse(recommendedCrewExperience.Value, ignoreCase: true, out RecommendedCrewExperience); + } if (SubmarineElement?.Attribute("type") != null) { @@ -341,6 +371,10 @@ namespace Barotrauma { OutpostModuleInfo = new OutpostModuleInfo(this, SubmarineElement); } + else if (Type == SubmarineType.BeaconStation) + { + BeaconStationInfo = new BeaconStationInfo(this, SubmarineElement); + } } } @@ -359,20 +393,6 @@ namespace Barotrauma SubmarineClass = SubmarineClass.Undefined; } - //backwards compatibility (use text tags instead of the actual text) - if (RecommendedCrewExperience == "Beginner") - { - RecommendedCrewExperience = "CrewExperienceLow"; - } - else if (RecommendedCrewExperience == "Intermediate") - { - RecommendedCrewExperience = "CrewExperienceMid"; - } - else if (RecommendedCrewExperience == "Experienced") - { - RecommendedCrewExperience = "CrewExperienceHigh"; - } - RequiredContentPackages.Clear(); string[] contentPackageNames = SubmarineElement.GetAttributeStringArray("requiredcontentpackages", Array.Empty()); foreach (string contentPackageName in contentPackageNames) @@ -528,6 +548,11 @@ namespace Barotrauma OutpostModuleInfo.Save(newElement); OutpostModuleInfo = new OutpostModuleInfo(this, newElement); } + else if (Type == SubmarineType.BeaconStation) + { + BeaconStationInfo.Save(newElement); + BeaconStationInfo = new BeaconStationInfo(this, newElement); + } XDocument doc = new XDocument(newElement); doc.Root.Add(new XAttribute("name", Name)); @@ -590,6 +615,7 @@ namespace Barotrauma List filePaths = new List(); foreach (BaseSubFile subFile in contentPackageSubs) { + if (!File.Exists(subFile.Path.Value)) { continue; } if (!filePaths.Any(fp => fp == subFile.Path)) { filePaths.Add(subFile.Path.Value); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs index b999e8e54..144a567ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs @@ -158,7 +158,7 @@ namespace Barotrauma.Networking private static void UpdateRead() { - Span msgLengthSpan = stackalloc byte[3]; + Span msgLengthSpan = stackalloc byte[4 + 1]; while (!shutDown) { CheckPipeConnected(nameof(readStream), readStream); @@ -182,8 +182,11 @@ namespace Barotrauma.Networking if (!readBytes(msgLengthSpan)) { shutDown = true; break; } - int msgLength = msgLengthSpan[0] | (msgLengthSpan[1] << 8); - WriteStatus writeStatus = (WriteStatus)msgLengthSpan[2]; + int msgLength = msgLengthSpan[0] + | (msgLengthSpan[1] << 8) + | (msgLengthSpan[2] << 16) + | (msgLengthSpan[3] << 24); + WriteStatus writeStatus = (WriteStatus)msgLengthSpan[4]; if (msgLength > 0) { @@ -225,12 +228,15 @@ namespace Barotrauma.Networking // when the function returns; placing it in the loop // this method is based around would lead to a stack // overflow real quick! - Span bytesToWrite = stackalloc byte[3 + msg.Length]; + Span bytesToWrite = stackalloc byte[4 + 1 + msg.Length]; bytesToWrite[0] = (byte)(msg.Length & 0xFF); bytesToWrite[1] = (byte)((msg.Length >> 8) & 0xFF); - bytesToWrite[2] = (byte)writeStatus; - Span msgSlice = bytesToWrite.Slice(3, msg.Length); + bytesToWrite[2] = (byte)((msg.Length >> 16) & 0xFF); + bytesToWrite[3] = (byte)((msg.Length >> 24) & 0xFF); + + bytesToWrite[4] = (byte)writeStatus; + Span msgSlice = bytesToWrite.Slice(4 + 1, msg.Length); msg.AsSpan().CopyTo(msgSlice); @@ -284,6 +290,12 @@ namespace Barotrauma.Networking { if (shutDown) { return; } + if (msg.Length > 0x1fff_ffff) + { + //This message is extremely long and is close to breaking + //ChildServerRelay, so let's not allow this to go through! + return; + } msgsToWrite.Enqueue(msg); writeManualResetEvent.Set(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index b8c2d6419..afa43e536 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -28,6 +28,8 @@ namespace Barotrauma.Networking REQUEST_STARTGAMEFINALIZE, //tell the server you're ready to finalize round initialization + UPDATE_CHARACTERINFO, + ERROR, //tell the server that an error occurred CREW, //hiring UI MEDICAL, //medical clinic diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index b5aae541a..7c612b9ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -900,27 +900,13 @@ namespace Barotrauma.Networking private set; } - [Serialize(true, IsPropertySaveable.Yes)] - public bool RadiationEnabled - { - get; - set; - } - [Serialize(LootedMoneyDestination.Bank, IsPropertySaveable.Yes)] public LootedMoneyDestination LootedMoneyDestination { get; set; } [Serialize(999999, IsPropertySaveable.Yes)] public int MaximumMoneyTransferRequest { get; set; } - private int maxMissionCount = CampaignSettings.DefaultMaxMissionCount; - - [Serialize(CampaignSettings.DefaultMaxMissionCount, IsPropertySaveable.Yes)] - public int MaxMissionCount - { - get { return maxMissionCount; } - set { maxMissionCount = MathHelper.Clamp(value, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit); } - } + public CampaignSettings CampaignSettings { get; set; } = CampaignSettings.Empty; private bool allowSubVoting; //Don't serialize: the value is set based on SubSelectionMode diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 99bb15486..8d73ee087 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -786,6 +786,9 @@ namespace Barotrauma case nameof(Character.HealthMultiplier): { if (parentObject is Character character) { character.StackHealthMultiplier(value); return true; } } break; + case nameof(Character.PropulsionSpeedMultiplier): + { if (parentObject is Character character) { character.PropulsionSpeedMultiplier = value; return true; } } + break; } return false; } @@ -799,6 +802,12 @@ namespace Barotrauma case nameof(Character.ObstructVision): { if (parentObject is Character character) { character.ObstructVision = value; return true; } } break; + case nameof(Character.HideFace): + { if (parentObject is Character character) { character.HideFace = value; return true; } } + break; + case nameof(Character.UseHullOxygen): + { if (parentObject is Character character) { character.UseHullOxygen = value; return true; } } + break; case nameof(LightComponent.IsOn): { if (parentObject is LightComponent lightComponent) { lightComponent.IsOn = value; return true; } } break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index 368cb94ab..600b2161d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -490,6 +490,10 @@ namespace Barotrauma { font.Prefabs.ForEach(p => p.LoadFont()); } + foreach (var componentStyle in GUIStyle.ComponentStyles) + { + componentStyle.RefreshSize(); + } } GameMain.SoundManager?.ApplySettings(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs index 025127e42..94e6c7bef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs @@ -94,7 +94,13 @@ namespace Barotrauma public override void Apply(ActionType type, float deltaTime, Entity entity, IReadOnlyList targets, Vector2? worldPosition = null) { - if (this.type != type || !HasRequiredItems(entity)) { return; } + if (this.type != type) { return; } + if (intervalTimer > 0.0f) + { + intervalTimer -= deltaTime; + return; + } + if (!HasRequiredItems(entity)) { return; } if (delayType == DelayTypes.ReachCursor && Character.Controlled == null) { return; } if (!Stackable) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index fd364e4f8..eebf72549 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -132,7 +132,11 @@ namespace Barotrauma public enum SpawnPositionType { This, + //the inventory of the StatusEffect's target entity ThisInventory, + //the same inventory the StatusEffect's target entity is in (only valid if the target is an Item) + SameInventory, + //the inventory of an item in the inventory of the StatusEffect's target entity (e.g. a container in the character's inventory) ContainedInventory } @@ -308,11 +312,26 @@ namespace Barotrauma private readonly float lifeTime; private float lifeTimer; + public float intervalTimer; + public static readonly List DurationList = new List(); - public readonly bool CheckConditionalAlways; //Always do the conditional checks for the duration/delay. If false, only check conditional on apply. + /// + /// Always do the conditional checks for the duration/delay. If false, only check conditional on apply. + /// + public readonly bool CheckConditionalAlways; - public readonly bool Stackable = true; //Can the same status effect be applied several times to the same targets? + /// + /// Only valid if the effect has a duration or delay. Can the effect be applied on the same target(s)s if the effect is already being applied? + /// + public readonly bool Stackable = true; + + /// + /// The interval at which the effect is executed. The difference between delay and interval is that effects with a delay find the targets, check the conditions, etc + /// immediately when Apply is called, but don't apply the effects until the delay has passed. Effects with an interval check if the interval has passed when Apply is + /// called and apply the effects if it has, otherwise they do nothing. + /// + public readonly float Interval; #if CLIENT private readonly bool playSoundOnRequiredItemFailure = false; @@ -450,6 +469,8 @@ namespace Barotrauma TargetSlot = element.GetAttributeInt("targetslot", -1); + Interval = element.GetAttributeFloat("interval", 0.0f); + Range = element.GetAttributeFloat("range", 0.0f); Offset = element.GetAttributeVector2("offset", Vector2.Zero); string[] targetLimbNames = element.GetAttributeStringArray("targetlimb", null) ?? element.GetAttributeStringArray("targetlimbs", null); @@ -556,6 +577,7 @@ namespace Barotrauma " - sounds should be defined as child elements of the StatusEffect, not as attributes."); break; case "delay": + case "interval": break; case "range": if (!HasTargetType(TargetType.NearbyCharacters) && !HasTargetType(TargetType.NearbyItems)) @@ -1094,6 +1116,12 @@ namespace Barotrauma { if (this.type != type) { return; } + if (intervalTimer > 0.0f) + { + intervalTimer -= deltaTime; + return; + } + currentTargets.Clear(); foreach (ISerializableEntity target in targets) { @@ -1195,7 +1223,11 @@ namespace Barotrauma lifeTimer -= deltaTime; if (lifeTimer <= 0) { return; } } - + if (intervalTimer > 0.0f) + { + intervalTimer -= deltaTime; + return; + } Hull hull = GetHull(entity); Vector2 position = GetPosition(entity, targets, worldPosition); if (useItemCount > 0) @@ -1717,6 +1749,26 @@ namespace Barotrauma } } 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) => + { + newItem.Condition = newItem.MaxCondition * chosenItemSpawnInfo.Condition; + }); + } + } + break; case ItemSpawnInfo.SpawnPositionType.ContainedInventory: { Inventory thisInventory = null; @@ -1756,6 +1808,8 @@ namespace Barotrauma ApplyProjSpecific(deltaTime, entity, targets, hull, position, playSound: true); + intervalTimer = Interval; + static Character CharacterFromTarget(ISerializableEntity target) { Character targetCharacter = target as Character; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index 46a58055b..7db7254a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -414,7 +414,7 @@ namespace Barotrauma.Steam { await Task.Yield(); Identifier extension = Path.GetExtension(from).ToIdentifier(); - if (extension == ".xml" && shouldCorrectPaths == ShouldCorrectPaths.Yes) + if (extension == ".xml") { try { @@ -427,10 +427,14 @@ namespace Barotrauma.Steam { throw new Exception($"Could not load \"{from}\": doc is null"); } - await CorrectPaths( - fileListDir: fileListDir, - modName: modName, - element: doc.Root ?? throw new NullReferenceException()); + + if (shouldCorrectPaths == ShouldCorrectPaths.Yes) + { + await CorrectPaths( + fileListDir: fileListDir, + modName: modName, + element: doc.Root ?? throw new NullReferenceException()); + } doc.SaveSafe(to); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs index c9152e8c1..73c6d6860 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs @@ -15,6 +15,21 @@ namespace Barotrauma public bool IsNone() => this is None; public bool IsSome() => this is Some; + public bool TryUnwrap(out T outValue) + { + switch (this) + { + case Some { Value: var value }: + outValue = value; + return true; + case None _: + outValue = default; + return false; + default: + throw new ArgumentOutOfRangeException(); + } + } + public Option Select(Func selector) => this switch { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs index b63e7a2bc..bb1950e0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs @@ -11,8 +11,8 @@ namespace Barotrauma public static Success Success(T value) => new Success(value); - public static Failure Failure(TError error, string? stackTrace) - => new Failure(error, stackTrace); + public static Failure Failure(TError error) + => new Failure(error); } public sealed class Success : Result @@ -34,14 +34,11 @@ namespace Barotrauma { public readonly TError Error; - public readonly string? StackTrace; - public override bool IsSuccess => false; - public Failure(TError error, string? stackTrace) + public Failure(TError error) { Error = error; - StackTrace = stackTrace; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs index ea2b7c2e8..651f13975 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs @@ -18,6 +18,23 @@ namespace Barotrauma.IO ".bat", ".sh", //shell scripts }.ToIdentifiers().ToImmutableArray(); + public ref struct Skipper + { + public void Dispose() + { + SkipValidationInDebugBuilds = false; + } + } + + /// + /// Skips validation for as long as the returned object remains in scope (remember to use using) + /// + public static Skipper SkipInDebugBuilds() + { + SkipValidationInDebugBuilds = true; + return new Skipper(); + } + /// /// When set to true, the game is allowed to modify the vanilla content in debug builds. Has no effect in non-debug builds. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index c73f40124..6ca456993 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -126,6 +126,10 @@ namespace Barotrauma public static void LoadGame(string filePath) { + //ensure there's no gamesession/sub loaded because it'd lead to issues when starting a new one (e.g. trying to determine which level to load based on the placement of the sub) + //can happen if a gamesession is interrupted ungracefully (exception during loading) + Submarine.Unload(); + GameMain.GameSession = null; DebugConsole.Log("Loading save file: " + filePath); DecompressToDirectory(filePath, TempPath, null); diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index ce02ef8f7..1cd44dc4e 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,76 @@ +--------------------------------------------------------------------------------------------------------- +v0.18.4.0 +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Option to select whether to automatically transfer the items from the old sub to a new one when switching the sub. +- Fixed Physicorium Chaingun Ammunition Box having an incorrect sprite. +- Fixed crashing when trying to load a save that includes ItemContainers with different kinds of inventory slots (e.g. PUCS). +- Disposable suits no longer protect from pressure when broken. +- Made disposable suits play a sound and their lights flicker when the suit is about to break. +- Fixed inability to access the character tab when your character is dead in non-campaign modes, fixed creating a new character not doing anything mid-round. +- Fixed sprite editor crashing if you try to reload a texture twice. +- Fixed drone/shuttles getting left behind in the outpost when you buy and switch to a sub that has a one. +- Fixed crashing when closing the submarine preview window. + +Changes: +- Added some new campaign settings: starting balance, amount of starting items and difficulty. +- Added two new beacon stations. +- Made the descriptions of some materials (that used to just say "useful for crafting") more descriptive. +- Increased oxygen generator output in some vanilla subs. +- Made handheld sonar beacon sound less grating. +- Disallowed mirroring beds vertically. +- The client who initiated a vote cannot take part in that vote (except if they're the only client who can vote, in which case the vote automatically passes). +- Made flashlight flicker before the battery runs out. +- Added some lootable money to corpses found in wrecks. +- Removed the small equipment indicators next to the character portrait. +- Added a bunch of new UI sounds (tickbox toggling, confirming transactions, increase/decrease sounds for number inputs, cart sound for adding/removing items in store interfaces, selecting/clicking components, sliders and modlist). + + +Fixes: +- Split campaign state networking messages into multiple ones. Previously all the campaign-related data (map state, reputation, upgrades, purchased items, selected missions) was included in the same message, and whenever anything in the data changed, the server would send all of it to clients. This would cause performance and bandwidth issues in some situations, for example when reputation was changing rapidly. +- Fixed some pumps in Kastrull working without power. +- Fixed quick-reloading working incorrectly when trying to reload from a stack that doesn't fully fit in the weapon (e.g. when double clicking on a full stack of revolver rounds with a half-loaded revolver in hand). +- Fixed inability to quick-reload weapons with more than 1 inventory slot (e.g. autoshotgun). +- Fixed outpost NPCs having x3 more health than they should. +- Fixed morbusine not killing NPCs with higher-than-default health. +- Fixed crashing with the error message "couldn't find a valid ICU package installed on the system" on some Linux distributions. +- Fixed graphics errors when using Razer Cortex overlay. +- Fixed bots being unable to repair Winterhalter's top hatch. +- Fixed server crashing if you disable all mission types and try to start a mission round. +- Fixed Chinese/Japanese/Korean text not wrapping properly on terminals. +- Fixed bots sometimes walking towards a wall or holding the ladders when they are idling. +- Fixed "main docking port" property not being taken into account when placing outposts (= the outpost was placed with the assumption that the docking port closest to the sub's center is the main docking port). Sometimes caused the outpost to be placed too close to the level walls, preventing the sub from docking with it. +- Fixed ladders not being visible in the sub preview. +- Fixed some UI elements being too large when switching from a large resolution to a smaller one, or vice versa. +- Fixed weapon holder sprite depth. +- Fixed level editor's test mode generating a different level than the editor itself. + +Modding: +- Added "mod lists" which can be used to enable/disable sets of mods more easily. +- Option to choose which local mod(s) to add a submarine to when saving one in the submarine editor. +- Mods can be unsubscribed from by right-clicking on them in the mod list, and it's possible to unsubscribe from multiple ones at the same time by using ctrl+click or shift+click to select more than one. +- Local mods can be merged in the mod list by selecting the ones you want to merge and selecting "merge all selected" from the right-click context menu. +- Better filtering in the mod list: option to only show local mods, Workshop mods, published mods, submarines and/or item assemblies. +- Added "SameInventory" spawn position type to status effects (allows spawning items in the same inventory the entity applying the effect is in). +- Added support for multiple light components in wearables. + +--------------------------------------------------------------------------------------------------------- +v0.18.3.0 +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Fixed submarine not getting saved between levels in the multiplayer campaign. +- Fixed clients sometimes unnecessarily showing the "trying to automatically dock with the outpost" prompt even when the docking is being done manually. +- Added a new texture and icon for shotgun rubber shell. +- Added sprite for disposable suit on shelf. + +Changes: +- TriggerComponent now supports negative forces: negative force value will cause the it to pull triggerers towards it. + +Modding: +- Multiple TriggerComponent properties can now be modified through signals and CustomInterface components. + --------------------------------------------------------------------------------------------------------- v0.18.2.0 --------------------------------------------------------------------------------------------------------- @@ -148,6 +221,17 @@ Modding: - Added an extra tag to the "canned heat" talent to make it easier to add custom upgradeable tanks that aren't compatible with vanilla tools. - Option to make status effects drop the items contained inside the target item (usage example in the duffel bag). +--------------------------------------------------------------------------------------------------------- +v0.17.16.0 +--------------------------------------------------------------------------------------------------------- + +Changes: +- Added some tutorial information to the data sent to GameAnalytics. + +Fixes: +- Fixed an exploit that allowed modified clients to execute console commands server-side without the appropriate permissions. +- Fixed NPCs spawning without any items when the system language is set to Turkish. + --------------------------------------------------------------------------------------------------------- v0.17.15.0 --------------------------------------------------------------------------------------------------------- From 6be757a45b7a65b422b538e962222fc2a6761920 Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Fri, 3 Jun 2022 22:29:04 +0900 Subject: [PATCH 05/14] Build 0.18.5.0 --- .../ClientSource/Characters/CharacterInfo.cs | 10 +- .../Characters/CharacterNetworking.cs | 2 + .../ClientSource/DebugConsole.cs | 4 +- .../ClientSource/GUI/ChatBox.cs | 140 +++++++++++++++++- .../ClientSource/GUI/ComponentStyle.cs | 4 + .../BarotraumaClient/ClientSource/GUI/GUI.cs | 40 +++-- .../ClientSource/GUI/GUIButton.cs | 5 + .../ClientSource/GUI/GUIDropDown.cs | 4 + .../ClientSource/GUI/GUITextBox.cs | 5 +- .../ClientSource/GUI/HUDLayoutSettings.cs | 13 +- .../ClientSource/GUI/Store.cs | 2 +- .../ClientSource/GUI/TabMenu.cs | 19 ++- .../ClientSource/GUI/UISprite.cs | 6 + .../BarotraumaClient/ClientSource/GameMain.cs | 6 +- .../ClientSource/GameSession/CrewManager.cs | 42 +----- .../GameModes/SinglePlayerCampaign.cs | 6 +- .../ClientSource/Items/Components/Door.cs | 123 ++++++++------- .../Items/Components/Holdable/IdCard.cs | 6 - .../ClientSource/Map/Submarine.cs | 94 +++++++----- .../ClientSource/Networking/ChatMessage.cs | 5 +- .../ClientSource/Networking/GameClient.cs | 48 ++---- .../Networking/OrderChatMessage.cs | 6 +- .../Networking/Voip/VoipCapture.cs | 17 +-- .../CampaignSetupUI/CampaignSetupUI.cs | 11 +- .../ClientSource/Screens/GameScreen.cs | 55 ++++++- .../ClientSource/Screens/NetLobbyScreen.cs | 7 +- .../ClientSource/Screens/SubEditorScreen.cs | 14 +- .../ClientSource/Settings/SettingsMenu.cs | 42 ++++-- .../ClientSource/Sounds/SoundManager.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/Networking/ChatMessage.cs | 7 +- .../ServerSource/Networking/GameServer.cs | 44 +++++- .../Networking/OrderChatMessage.cs | 6 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Data/campaignsettings.xml | 4 +- .../AI/Objectives/AIObjectiveCleanupItems.cs | 13 +- .../AI/Objectives/AIObjectiveIdle.cs | 2 +- .../AI/Objectives/AIObjectiveLoadItems.cs | 16 +- .../AI/Objectives/AIObjectivePumpWater.cs | 18 ++- .../AI/Objectives/AIObjectiveRepairItems.cs | 5 +- .../SharedSource/Characters/CharacterInfo.cs | 9 +- .../SharedSource/Characters/Jobs/JobPrefab.cs | 6 +- .../Params/Ragdoll/RagdollParams.cs | 2 +- .../ContentManagement/ContentFile/JobsFile.cs | 6 +- .../BarotraumaShared/SharedSource/Enums.cs | 7 + .../GameSession/GameModes/CampaignMode.cs | 11 +- .../GameModes/CampaignModePresets.cs | 14 +- .../SharedSource/InputType.cs | 5 +- .../SharedSource/Items/Components/Door.cs | 6 +- .../Items/Components/Holdable/Holdable.cs | 3 + .../Items/Components/Holdable/Pickable.cs | 3 +- .../Items/Components/Holdable/RepairTool.cs | 2 +- .../Items/Components/ItemContainer.cs | 17 ++- .../Items/Components/Machines/Controller.cs | 26 ++++ .../Items/Components/Power/Powered.cs | 4 +- .../SharedSource/Items/Components/Wearable.cs | 2 - .../SharedSource/Items/Item.cs | 67 +++++++-- .../Map/Creatures/BallastFloraBehavior.cs | 48 ++++-- .../SharedSource/Map/Levels/Level.cs | 11 +- .../SharedSource/Map/MapEntity.cs | 14 ++ .../SharedSource/Map/Submarine.cs | 24 ++- .../SharedSource/Networking/ChatMessage.cs | 2 + .../SharedSource/PerformanceCounter.cs | 50 ------- .../SharedSource/Screens/GameScreen.cs | 18 +-- .../Serialization/SerializableProperty.cs | 7 +- .../SharedSource/Settings/GameSettings.cs | 29 +++- .../StatusEffects/StatusEffect.cs | 6 +- Barotrauma/BarotraumaShared/changelog.txt | 44 +++++- 72 files changed, 869 insertions(+), 439 deletions(-) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index deca50826..305031f47 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -266,7 +266,7 @@ namespace Barotrauma disguisedSkinColor = idCard.StoredOwnerAppearance.SkinColor; } - partial void LoadAttachmentSprites(bool omitJob) + partial void LoadAttachmentSprites() { if (attachmentSprites == null) { @@ -280,14 +280,6 @@ namespace Barotrauma Head.BeardElement?.GetChildElements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.Beard))); Head.MoustacheElement?.GetChildElements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.Moustache))); Head.HairElement?.GetChildElements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.Hair))); - if (omitJob) - { - JobPrefab.NoJobElement?.GetChildElement("PortraitClothing")?.GetChildElements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.JobIndicator))); - } - else - { - Job?.Prefab.ClothingElement?.GetChildElements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.JobIndicator))); - } } // Doesn't work if the head's source rect does not start at 0,0. diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 7fd2cfdc0..c78789db0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -651,6 +651,8 @@ namespace Barotrauma GameMain.LightManager.LosEnabled = true; GameMain.LightManager.LosAlpha = 1f; + GameMain.NetLobbyScreen.CampaignCharacterDiscarded = false; + character.memInput.Clear(); character.memState.Clear(); character.memLocalState.Clear(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index c56a20796..b8cff8d92 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -172,6 +172,7 @@ namespace Barotrauma isOpen = false; GUI.ForceMouseOn(null); textBox.Deselect(); + SoundPlayer.PlayUISound(GUISoundType.Select); } if (isOpen) @@ -209,7 +210,7 @@ namespace Barotrauma isOpen = !isOpen; if (isOpen) { - textBox.Select(); + textBox.Select(ignoreSelectSound: true); AddToGUIUpdateList(); } else @@ -217,6 +218,7 @@ namespace Barotrauma GUI.ForceMouseOn(null); textBox.Deselect(); } + SoundPlayer.PlayUISound(GUISoundType.Select); } private static bool IsCommandPermitted(string command, GameClient client) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index 1a780c3ec..a6a44048a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -81,6 +81,8 @@ namespace Barotrauma public const int ToggleButtonWidthRaw = 30; private int popupMessageOffset; + private GUIDropDown ChatModeDropDown { get; set; } + public ChatBox(GUIComponent parent, bool isSinglePlayer) { this.IsSinglePlayer = isSinglePlayer; @@ -226,7 +228,53 @@ namespace Barotrauma // --------------------------------------------------------------------------------------------- - InputBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.125f), hideableElements.RectTransform, Anchor.BottomLeft), + var bottomContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.125f), hideableElements.RectTransform, Anchor.BottomLeft), isHorizontal: true) + { + Stretch = true + }; + + var dropdownRt = new RectTransform(new Vector2(0.1f, 1.0f), bottomContainer.RectTransform) + { + // The chat mode selection dropdown will take a maximum of 45% of the horizontal space + MaxSize = new Point((int)(0.45f * bottomContainer.RectTransform.NonScaledSize.X), int.MaxValue) + }; + var chatModes = new ChatMode[] { ChatMode.Local, ChatMode.Radio }; + ChatModeDropDown = new GUIDropDown(dropdownRt, elementCount: chatModes.Length, dropAbove: true) + { + OnSelected = (component, userdata) => + { + GameMain.ActiveChatMode = (ChatMode)userdata; + if (InputBox != null && InputBox.Text.StartsWith(RadioChatString) && GameMain.ActiveChatMode == ChatMode.Local) + { + string text = InputBox.Text; + InputBox.Text = text.Remove(0, RadioChatString.Length); + } + return true; + } + }; + float longestDropDownOption = 0.0f; + foreach (ChatMode mode in chatModes) + { + var text = TextManager.Get($"chatmode.{mode}"); + ChatModeDropDown.AddItem(text, userData: mode); + if (ChatModeDropDown.ListBox.Content.GetChildByUserData(mode) is GUITextBlock textBlock) + { + if (textBlock.TextSize.X > longestDropDownOption) + { + longestDropDownOption = textBlock.TextSize.X; + } + } + } + ChatModeDropDown.SelectItem(GameMain.ActiveChatMode); + + float minDropDownWidth = longestDropDownOption + ChatModeDropDown.Padding.X + + (ChatModeDropDown.DropDownIcon?.RectTransform.NonScaledSize.X ?? 0) + + (ChatModeDropDown.DropDownIcon?.RectTransform.AbsoluteOffset.X ?? 0) * 2; + ChatModeDropDown.RectTransform.MinSize = new Point( + Math.Max((int)minDropDownWidth, ChatModeDropDown.RectTransform.MinSize.X), + ChatModeDropDown.RectTransform.MinSize.Y); + + InputBox = new GUITextBox(new RectTransform(new Vector2(0.9f, 1.0f), bottomContainer.RectTransform), style: "ChatTextBox") { OverflowClip = true, @@ -248,8 +296,6 @@ namespace Barotrauma CloseAfterMessageSent = false; } } - - //gui.Text = ""; }; var chatSendButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.7f), InputBox.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight), style: "GUIButtonToggleRight"); @@ -306,6 +352,10 @@ namespace Barotrauma { textColor = ChatMessage.MessageColor[(int)ChatMessageType.Private]; } + else if (GameMain.ActiveChatMode == ChatMode.Radio) + { + textColor = ChatMessage.MessageColor[(int)ChatMessageType.Radio]; + } else { textColor = ChatMessage.MessageColor[(int)ChatMessageType.Default]; @@ -550,6 +600,25 @@ namespace Barotrauma showNewMessagesButton.Visible = false; } + if (PlayerInput.KeyHit(InputType.ToggleChatMode) && GUI.KeyboardDispatcher.Subscriber == null && Screen.Selected == GameMain.GameScreen) + { + try + { + var mode = GameMain.ActiveChatMode switch + { + ChatMode.Local => ChatMode.Radio, + ChatMode.Radio => ChatMode.Local, + _ => throw new NotImplementedException() + }; + ChatModeDropDown.SelectItem(mode); + // TODO: Play a sound? + } + catch (NotImplementedException) + { + DebugConsole.ThrowError($"Error toggling chat mode: not implemented for current mode \"{GameMain.ActiveChatMode}\""); + } + } + if (ToggleButton != null) { ToggleButton.Selected = ToggleOpen; @@ -700,5 +769,70 @@ namespace Barotrauma } } } + + public void ApplySelectionInputs() => ApplySelectionInputs(InputBox, true, ChatKeyStates.GetChatKeyStates()); + + public struct ChatKeyStates + { + public bool ActiveChatKeyHit { get; set; } + public bool LocalChatKeyHit { get; set; } + public bool RadioChatKeyHit { get; set; } + public bool AnyHit => ActiveChatKeyHit || LocalChatKeyHit || RadioChatKeyHit; + + private ChatKeyStates(bool active, bool local, bool radio) + { + ActiveChatKeyHit = active; + LocalChatKeyHit = local; + RadioChatKeyHit = radio; + } + + public static ChatKeyStates GetChatKeyStates() + { + return new ChatKeyStates(PlayerInput.KeyHit(InputType.ActiveChat), + PlayerInput.KeyHit(InputType.Chat), + PlayerInput.KeyHit(InputType.RadioChat) && (Character.Controlled == null || Character.Controlled.SpeechImpediment < 100)); + } + + public (bool active, bool local, bool radio) Deconstruct() + { + return (ActiveChatKeyHit, LocalChatKeyHit, RadioChatKeyHit); + } + } + + public void ApplySelectionInputs(GUITextBox inputBox, bool selectInputBox, ChatKeyStates chatKeyStates) + { + inputBox ??= InputBox; + var (activeChatKeyHit, localChatKeyHit, radioChatKeyHit) = chatKeyStates.Deconstruct(); + if (localChatKeyHit || (activeChatKeyHit && GameMain.ActiveChatMode == ChatMode.Local)) + { + ChatModeDropDown.SelectItem(ChatMode.Local); + inputBox.AddToGUIUpdateList(); + GUIFrame.Flash(Color.DarkGreen, 0.5f); + if (!ToggleOpen) + { + CloseAfterMessageSent = !ToggleOpen; + ToggleOpen = true; + } + if (selectInputBox) + { + inputBox.Select(inputBox.Text.Length); + } + } + else if (radioChatKeyHit || (activeChatKeyHit && GameMain.ActiveChatMode == ChatMode.Radio)) + { + ChatModeDropDown.SelectItem(ChatMode.Radio); + inputBox.AddToGUIUpdateList(); + GUIFrame.Flash(Color.YellowGreen, 0.5f); + if (!ToggleOpen) + { + CloseAfterMessageSent = !ToggleOpen; + ToggleOpen = true; + } + if (selectInputBox) + { + inputBox.Select(inputBox.Text.Length); + } + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs index d5707afa4..9a33f0ce9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs @@ -183,6 +183,10 @@ namespace Barotrauma Width = null; Height = null; GetSize(Element); + foreach (var childStyle in ChildStyles.Values) + { + childStyle.RefreshSize(); + } } private void GetSize(XElement element) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 251799b66..49f027c19 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -356,7 +356,7 @@ namespace Barotrauma GUIStyle.Green, Color.Black * 0.8f, font: GUIStyle.SmallFont); y += yStep; GameMain.PerformanceCounter.DrawTimeGraph.Draw(spriteBatch, new Rectangle((int)x, (int)y, 170, 50), color: GUIStyle.Green); - y += yStep * 3; + y += yStep * 4; DrawString(spriteBatch, new Vector2(x, y), "Update - Avg: " + GameMain.PerformanceCounter.UpdateTimeGraph.Average().ToString("0.00") + " ms" + @@ -364,39 +364,33 @@ namespace Barotrauma Color.LightBlue, Color.Black * 0.8f, font: GUIStyle.SmallFont); y += yStep; GameMain.PerformanceCounter.UpdateTimeGraph.Draw(spriteBatch, new Rectangle((int)x, (int)y, 170, 50), color: Color.LightBlue); - y += yStep * 3; - foreach (string key in GameMain.PerformanceCounter.GetSavedIdentifiers) + y += yStep * 4; + foreach (string key in GameMain.PerformanceCounter.GetSavedIdentifiers.OrderBy(i => i)) { float elapsedMillisecs = GameMain.PerformanceCounter.GetAverageElapsedMillisecs(key); - DrawString(spriteBatch, new Vector2(x, y), - key + ": " + elapsedMillisecs.ToString("0.00"), - Color.Lerp(Color.LightGreen, GUIStyle.Red, elapsedMillisecs / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - y += yStep; - foreach (string childKey in GameMain.PerformanceCounter.GetSavedPartialIdentifiers(key)) - { - elapsedMillisecs = GameMain.PerformanceCounter.GetPartialAverageElapsedMillisecs(key, childKey); - DrawString(spriteBatch, new Vector2(x + 15, y), - childKey + ": " + elapsedMillisecs.ToString("0.00"), - Color.Lerp(Color.LightGreen, GUIStyle.Red, elapsedMillisecs / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - y += yStep; - } - } + int categoryDepth = key.Count(c => c == ':'); + //color the more fine-grained counters red more easily (ok for the whole Update to take a longer time than specific part of the update) + float runningSlowThreshold = 10.0f / categoryDepth; + DrawString(spriteBatch, new Vector2(x + categoryDepth * 15, y), + key.Split(':').Last() + ": " + elapsedMillisecs.ToString("0.00"), + ToolBox.GradientLerp(elapsedMillisecs / runningSlowThreshold, Color.LightGreen, GUIStyle.Yellow, GUIStyle.Orange, GUIStyle.Red, Color.Magenta), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + y += yStep; + } if (Powered.Grids != null) { DrawString(spriteBatch, new Vector2(x, y), "Grids: " + Powered.Grids.Count, Color.LightGreen, Color.Black * 0.5f, 0, GUIStyle.SmallFont); y += yStep; } - if (Settings.EnableDiagnostics) { x += yStep * 2; - DrawString(spriteBatch, new Vector2(x, y), "ContinuousPhysicsTime: " + GameMain.World.ContinuousPhysicsTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ContinuousPhysicsTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - DrawString(spriteBatch, new Vector2(x, y + yStep), "ControllersUpdateTime: " + GameMain.World.ControllersUpdateTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ControllersUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - DrawString(spriteBatch, new Vector2(x, y + yStep * 2), "AddRemoveTime: " + GameMain.World.AddRemoveTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.AddRemoveTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - DrawString(spriteBatch, new Vector2(x, y + yStep * 3), "NewContactsTime: " + GameMain.World.NewContactsTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.NewContactsTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - DrawString(spriteBatch, new Vector2(x, y + yStep * 4), "ContactsUpdateTime: " + GameMain.World.ContactsUpdateTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ContactsUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - DrawString(spriteBatch, new Vector2(x, y + yStep * 5), "SolveUpdateTime: " + GameMain.World.SolveUpdateTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.SolveUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + DrawString(spriteBatch, new Vector2(x, y), "ContinuousPhysicsTime: " + GameMain.World.ContinuousPhysicsTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ContinuousPhysicsTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + DrawString(spriteBatch, new Vector2(x, y + yStep), "ControllersUpdateTime: " + GameMain.World.ControllersUpdateTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ControllersUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + DrawString(spriteBatch, new Vector2(x, y + yStep * 2), "AddRemoveTime: " + GameMain.World.AddRemoveTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.AddRemoveTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + DrawString(spriteBatch, new Vector2(x, y + yStep * 3), "NewContactsTime: " + GameMain.World.NewContactsTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.NewContactsTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + DrawString(spriteBatch, new Vector2(x, y + yStep * 4), "ContactsUpdateTime: " + GameMain.World.ContactsUpdateTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ContactsUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + DrawString(spriteBatch, new Vector2(x, y + yStep * 5), "SolveUpdateTime: " + GameMain.World.SolveUpdateTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.SolveUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs index 09e8107b1..a824218e5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs @@ -111,6 +111,11 @@ namespace Barotrauma set { textBlock.SelectedTextColor = value; } } + public Color DisabledTextColor + { + get { return textBlock.DisabledTextColor; } + } + public override float FlashTimer { get { return Frame.FlashTimer; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index 389456b9a..f8e908d8d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -160,6 +160,10 @@ namespace Barotrauma listBox.ToolTip = value; } } + + public GUIImage DropDownIcon => icon; + + public Vector4 Padding => button.TextBlock.Padding; public GUIDropDown(RectTransform rectT, LocalizedString text = null, int elementCount = 4, string style = "", bool selectMultiple = false, bool dropAbove = false, Alignment textAlignment = Alignment.CenterLeft) : base(style, rectT) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index fcbf3a5f4..866214dd6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -352,7 +352,7 @@ namespace Barotrauma caretPosDirty = false; } - public void Select(int forcedCaretIndex = -1) + public void Select(int forcedCaretIndex = -1, bool ignoreSelectSound = false) { skipUpdate = true; if (memento.Current == null) @@ -362,10 +362,11 @@ namespace Barotrauma CaretIndex = forcedCaretIndex == - 1 ? textBlock.GetCaretIndexFromScreenPos(PlayerInput.MousePosition) : forcedCaretIndex; CalculateCaretPos(); ClearSelection(); + bool wasSelected = selected; selected = true; GUI.KeyboardDispatcher.Subscriber = this; OnSelected?.Invoke(this, Keys.None); - if (PlaySoundOnSelect) + if (!wasSelected && PlaySoundOnSelect && !ignoreSelectSound) { SoundPlayer.PlayUISound(GUISoundType.Select); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index f255c8f5c..7afce155d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Linq; namespace Barotrauma { @@ -122,7 +123,17 @@ namespace Barotrauma //horizontal slices at the corners of the screen for health bar and affliction icons int afflictionAreaHeight = (int)(50 * GUI.Scale); - int healthBarWidth = (int)(BottomRightInfoArea.Width * 1.3f); + int healthBarWidth = BottomRightInfoArea.Width; + + var healthBarChildStyles = GUIStyle.GetComponentStyle("CharacterHealthBar")?.ChildStyles; + if (healthBarChildStyles!= null && healthBarChildStyles.TryGetValue("GUIFrame".ToIdentifier(), out var style)) + { + if (style.Sprites.TryGetValue(GUIComponent.ComponentState.None, out var uiSprites) && uiSprites.FirstOrDefault() is { } uiSprite) + { + // The default health bar uses a sliced sprite so let's make sure the health bar area is calculated accordingly + healthBarWidth += (int)(uiSprite.NonSliceSize.X * Math.Min(GUI.Scale, 1f)); + } + } int healthBarHeight = (int)(50f * GUI.Scale); HealthBarArea = new Rectangle(BottomRightInfoArea.Right - healthBarWidth + (int)Math.Floor(1 / GUI.Scale), BottomRightInfoArea.Y - healthBarHeight + GUI.IntScale(10), healthBarWidth, healthBarHeight); AfflictionAreaLeft = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 105bd7a07..f27eed481 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -2208,7 +2208,7 @@ namespace Barotrauma } updateStopwatch.Stop(); - GameMain.PerformanceCounter.AddPartialElapsedTicks("GameSessionUpdate", "StoreUpdate", updateStopwatch.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:GameSession:Store", updateStopwatch.ElapsedTicks); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index cbf116f0b..47752417d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -1858,14 +1858,18 @@ namespace Barotrauma }); GUILayoutGroup nameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1f), talentInfoLayoutGroup.RectTransform)) { RelativeSpacing = 0.05f }; - + Vector2 nameSize = GUIStyle.SubHeadingFont.MeasureString(info.Name); - GUITextBlock nameBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont) { TextColor = job.Prefab.UIColor }; + GUITextBlock nameBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont); nameBlock.RectTransform.NonScaledSize = nameSize.Pad(nameBlock.Padding).ToPoint(); - Vector2 jobSize = GUIStyle.SmallFont.MeasureString(job.Name); - GUITextBlock jobBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), job.Name, font: GUIStyle.SmallFont) { TextColor = job.Prefab.UIColor }; - jobBlock.RectTransform.NonScaledSize = jobSize.Pad(jobBlock.Padding).ToPoint(); + if (!info.OmitJobInMenus) + { + nameBlock.TextColor = job.Prefab.UIColor; + Vector2 jobSize = GUIStyle.SmallFont.MeasureString(job.Name); + GUITextBlock jobBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), job.Name, font: GUIStyle.SmallFont) { TextColor = job.Prefab.UIColor }; + jobBlock.RectTransform.NonScaledSize = jobSize.Pad(jobBlock.Padding).ToPoint(); + } LocalizedString traitString = TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), TextManager.Get("personalitytrait." + info.PersonalityTrait.Name.Replace(" ", ""))); Vector2 traitSize = GUIStyle.SmallFont.MeasureString(traitString); @@ -1876,7 +1880,8 @@ namespace Barotrauma if (!(GameMain.NetworkMember is null)) { - GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.675f, 1f), talentsOutsideTreeFrame.RectTransform, Anchor.TopLeft), text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew")) + GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.675f, 1f), talentsOutsideTreeFrame.RectTransform, Anchor.TopLeft), + text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew")) { IgnoreLayoutGroups = true }; @@ -1915,7 +1920,7 @@ namespace Barotrauma { OnClicked = (button, o) => { - GameMain.Client?.SendCharacterInfo(); + GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName); characterSettingsFrame!.Visible = false; talentFrameMain.Visible = true; return true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs index 54c5ee076..fc8dfd1f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs @@ -27,6 +27,11 @@ namespace Barotrauma set; } + /// + /// The size of fixed area around the slice area + /// + public Point NonSliceSize { get; set; } + public bool MaintainAspectRatio { get; @@ -72,6 +77,7 @@ namespace Barotrauma maxBorderScale = element.GetAttributeFloat("minborderscale", 10.0f); Rectangle slice = new Rectangle((int)sliceVec.X, (int)sliceVec.Y, (int)(sliceVec.Z - sliceVec.X), (int)(sliceVec.W - sliceVec.Y)); + NonSliceSize = new Point(Sprite.SourceRect.Width - slice.Width, Sprite.SourceRect.Height - slice.Height); Slices = new Rectangle[9]; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 01b319c25..7f55a1968 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -202,6 +202,8 @@ namespace Barotrauma public static bool CancelQuickStart; #endif + public static ChatMode ActiveChatMode { get; set; } = ChatMode.Radio; + public GameMain(string[] args) { Content.RootDirectory = "Content"; @@ -924,7 +926,7 @@ namespace Barotrauma updateCount++; sw.Stop(); - PerformanceCounter.AddElapsedTicks("Update total", sw.ElapsedTicks); + PerformanceCounter.AddElapsedTicks("Update", sw.ElapsedTicks); PerformanceCounter.UpdateTimeGraph.Update(sw.ElapsedTicks * 1000.0f / (float)Stopwatch.Frequency); } @@ -1026,7 +1028,7 @@ namespace Barotrauma } sw.Stop(); - PerformanceCounter.AddElapsedTicks("Draw total", sw.ElapsedTicks); + PerformanceCounter.AddElapsedTicks("Draw", sw.ElapsedTicks); PerformanceCounter.DrawTimeGraph.Update(sw.ElapsedTicks * 1000.0f / (float)Stopwatch.Frequency); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 52fa7e3cd..a4f756646 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -147,9 +147,10 @@ namespace Barotrauma string msgCommand = ChatMessage.GetChatMessageCommand(text, out string msg); // add to local history ChatBox.ChatManager.Store(text); - WifiComponent headset = null; - ChatMessageType messageType = - ((msgCommand == "r" || msgCommand == "radio") && ChatMessage.CanUseRadio(Character.Controlled, out headset)) ? ChatMessageType.Radio : ChatMessageType.Default; + bool isUsingRadioMode = GameMain.ActiveChatMode == ChatMode.Radio; + bool containsRadioCommand = msgCommand == "r" || msgCommand == "radio"; + bool canUseRadio = ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent headset); + ChatMessageType messageType = ((isUsingRadioMode && msgCommand == "") || containsRadioCommand) && canUseRadio ? ChatMessageType.Radio : ChatMessageType.Default; AddSinglePlayerChatMessage( Character.Controlled.Info.Name, msg, messageType, @@ -1553,40 +1554,9 @@ namespace Barotrauma { ChatBox.Update(deltaTime); ChatBox.InputBox.Visible = Character.Controlled != null; - - if (!DebugConsole.IsOpen && ChatBox.InputBox.Visible && GUI.KeyboardDispatcher.Subscriber == null) + if (!DebugConsole.IsOpen && ChatBox.InputBox.Visible && GUI.KeyboardDispatcher.Subscriber == null && !ChatBox.InputBox.Selected) { - if (PlayerInput.KeyHit(InputType.Chat) && !ChatBox.InputBox.Selected) - { - ChatBox.InputBox.AddToGUIUpdateList(); - ChatBox.GUIFrame.Flash(Color.DarkGreen, 0.5f); - if (!ChatBox.ToggleOpen) - { - ChatBox.CloseAfterMessageSent = !ChatBox.ToggleOpen; - ChatBox.ToggleOpen = true; - } - ChatBox.InputBox.Select(ChatBox.InputBox.Text.Length); - } - - if (PlayerInput.KeyHit(InputType.RadioChat) && !ChatBox.InputBox.Selected) - { - if (Character.Controlled == null || Character.Controlled.SpeechImpediment < 100) - { - ChatBox.InputBox.AddToGUIUpdateList(); - ChatBox.GUIFrame.Flash(Color.YellowGreen, 0.5f); - if (!ChatBox.ToggleOpen) - { - ChatBox.CloseAfterMessageSent = !ChatBox.ToggleOpen; - ChatBox.ToggleOpen = true; - } - - if (!ChatBox.InputBox.Text.StartsWith(ChatBox.RadioChatString)) - { - ChatBox.InputBox.Text = ChatBox.RadioChatString; - } - ChatBox.InputBox.Select(ChatBox.InputBox.Text.Length); - } - } + ChatBox.ApplySelectionInputs(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index ed5e25c86..065bfeb8a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -35,11 +35,7 @@ namespace Barotrauma } } - if (CrewManager.ChatBox != null) - { - CrewManager.ChatBox.Update(deltaTime); - } - + CrewManager.ChatBox?.Update(deltaTime); CrewManager.UpdateReports(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index 9ea53f8da..3d3070aef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -43,6 +43,31 @@ namespace Barotrauma.Items.Components corners[2] = center + new Vector2(shadowSize.X, shadowSize.Y) / 2; corners[3] = center + new Vector2(shadowSize.X, -shadowSize.Y) / 2; + if (IsHorizontal) + { + if (item.FlippedX) + { + Vector2 itemCenter = new Vector2(item.Rect.Center.X, item.Rect.Y - item.Rect.Height / 2); + for (int i = 0; i < corners.Length; i++) + { + corners[i].X = itemCenter.X * 2 - corners[i].X; + } + Array.Reverse(corners); + } + } + else + { + if (item.FlippedY) + { + Vector2 itemCenter = new Vector2(item.Rect.Center.X, item.Rect.Y - item.Rect.Height / 2); + for (int i = 0; i < corners.Length; i++) + { + corners[i].Y = itemCenter.Y * 2 - corners[i].Y; + } + Array.Reverse(corners); + } + } + return corners; } @@ -163,69 +188,67 @@ namespace Barotrauma.Items.Components if (stuck > 0.0f && weldedSprite != null) { Vector2 weldSpritePos = new Vector2(item.Rect.Center.X, item.Rect.Y - item.Rect.Height / 2.0f) + shakePos; - if (item.Submarine != null) weldSpritePos += item.Submarine.DrawPosition; + if (item.Submarine != null) { weldSpritePos += item.Submarine.DrawPosition; } weldSpritePos.Y = -weldSpritePos.Y; weldedSprite.Draw(spriteBatch, weldSpritePos, item.SpriteColor * (stuck / 100.0f), scale: item.Scale); } - if (openState >= 1.0f) - { - return; - } + if (openState >= 1.0f) { return; } + Vector2 pos; if (IsHorizontal) { - Vector2 pos = new Vector2(item.Rect.X, item.Rect.Y - item.Rect.Height / 2) + shakePos; - if (item.Submarine != null) pos += item.Submarine.DrawPosition; - pos.Y = -pos.Y; - - if (brokenSprite == null || !IsBroken) - { - spriteBatch.Draw(doorSprite.Texture, pos, - new Rectangle((int) (doorSprite.SourceRect.X + doorSprite.size.X * openState), - (int) doorSprite.SourceRect.Y, - (int) (doorSprite.size.X * (1.0f - openState)), (int) doorSprite.size.Y), - color, 0.0f, doorSprite.Origin, item.Scale, SpriteEffects.None, doorSprite.Depth); - } - - if (brokenSprite != null && item.Health < item.MaxCondition) - { - Vector2 scale = scaleBrokenSprite ? new Vector2(1.0f, 1.0f - item.Health / item.MaxCondition) : Vector2.One; - float alpha = fadeBrokenSprite ? 1.0f - item.Health / item.MaxCondition : 1.0f; - spriteBatch.Draw(brokenSprite.Texture, pos, - new Rectangle((int)(brokenSprite.SourceRect.X + brokenSprite.size.X * openState), brokenSprite.SourceRect.Y, - (int)(brokenSprite.size.X * (1.0f - openState)), (int)brokenSprite.size.Y), - color * alpha, 0.0f, brokenSprite.Origin, scale * item.Scale, SpriteEffects.None, - brokenSprite.Depth); - } + pos = new Vector2(item.Rect.X, item.Rect.Y - item.Rect.Height / 2); + if (item.FlippedX) { pos.X += (int)(doorSprite.size.X * item.Scale * openState); } } else { - Vector2 pos = new Vector2(item.Rect.Center.X, item.Rect.Y) + shakePos; - if (item.Submarine != null) pos += item.Submarine.DrawPosition; - pos.Y = -pos.Y; - - if (brokenSprite == null || !IsBroken) - { - spriteBatch.Draw(doorSprite.Texture, pos, - new Rectangle(doorSprite.SourceRect.X, - (int) (doorSprite.SourceRect.Y + doorSprite.size.Y * openState), - (int) doorSprite.size.X, (int) (doorSprite.size.Y * (1.0f - openState))), - color, 0.0f, doorSprite.Origin, item.Scale, SpriteEffects.None, doorSprite.Depth); - } - - if (brokenSprite != null && item.Health < item.MaxCondition) - { - Vector2 scale = scaleBrokenSprite ? new Vector2(1.0f - item.Health / item.MaxCondition, 1.0f) : Vector2.One; - float alpha = fadeBrokenSprite ? 1.0f - item.Health / item.MaxCondition : 1.0f; - spriteBatch.Draw(brokenSprite.Texture, pos, - new Rectangle(brokenSprite.SourceRect.X, (int)(brokenSprite.SourceRect.Y + brokenSprite.size.Y * openState), - (int)brokenSprite.size.X, (int)(brokenSprite.size.Y * (1.0f - openState))), - color * alpha, 0.0f, brokenSprite.Origin, scale * item.Scale, SpriteEffects.None, brokenSprite.Depth); - } + pos = new Vector2(item.Rect.Center.X, item.Rect.Y); + if (item.FlippedY) { pos.Y -= (int)(doorSprite.size.Y * item.Scale * openState); } } + + pos += shakePos; + if (item.Submarine != null) { pos += item.Submarine.DrawPosition; } + pos.Y = -pos.Y; + + if (brokenSprite == null || !IsBroken) + { + spriteBatch.Draw(doorSprite.Texture, pos, + getSourceRect(doorSprite, openState, IsHorizontal), + color, 0.0f, doorSprite.Origin, item.Scale, item.SpriteEffects, doorSprite.Depth); + } + + if (brokenSprite != null && item.Health < item.MaxCondition) + { + Vector2 scale = scaleBrokenSprite ? new Vector2(1.0f, 1.0f - item.Health / item.MaxCondition) : Vector2.One; + float alpha = fadeBrokenSprite ? 1.0f - item.Health / item.MaxCondition : 1.0f; + spriteBatch.Draw(brokenSprite.Texture, pos, + getSourceRect(brokenSprite, openState, IsHorizontal), + color * alpha, 0.0f, brokenSprite.Origin, scale * item.Scale, item.SpriteEffects, + brokenSprite.Depth); + } + + static Rectangle getSourceRect(Sprite sprite, float openState, bool horizontal) + { + if (horizontal) + { + return new Rectangle( + (int)(sprite.SourceRect.X + sprite.size.X * openState), + sprite.SourceRect.Y, + (int)(sprite.size.X * (1.0f - openState)), + (int)sprite.size.Y); + } + else + { + return new Rectangle( + sprite.SourceRect.X, + (int)(sprite.SourceRect.Y + sprite.size.Y * openState), + (int)sprite.size.X, + (int)(sprite.size.Y * (1.0f - openState))); + } + } } partial void OnFailedToOpen() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs index 23083323d..bd2b6da47 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs @@ -116,12 +116,6 @@ namespace Barotrauma.Items.Components loadAttachments(Attachments, disguisedBeardElement, WearableType.Beard); loadAttachments(Attachments, disguisedMoustacheElement, WearableType.Moustache); loadAttachments(Attachments, disguisedHairElement, WearableType.Hair); - - loadAttachments(Attachments, - characterInfo.OmitJobInPortraitClothing - ? JobPrefab.NoJobElement?.GetChildElement("PortraitClothing") - : JobPrefab?.ClothingElement, - WearableType.JobIndicator); } HairColor = hairColor; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 45218c779..44c3c5e92 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -1,55 +1,61 @@ -using Barotrauma.Networking; -using Barotrauma.RuinGeneration; -using Barotrauma.Sounds; +using Barotrauma.Items.Components; +using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; -using Barotrauma.IO; using System.Linq; -using System.Xml.Linq; -using Barotrauma.Items.Components; namespace Barotrauma { partial class Submarine : Entity, IServerPositionSync { - public static Vector2 MouseToWorldGrid(Camera cam, Submarine sub) - { - Vector2 position = PlayerInput.MousePosition; - position = cam.ScreenToWorld(position); - - Vector2 worldGridPos = VectorToWorldGrid(position); - - if (sub != null) - { - worldGridPos.X += sub.Position.X % GridSize.X; - worldGridPos.Y += sub.Position.Y % GridSize.Y; - } - - return worldGridPos; - } - //drawing ---------------------------------------------------- private static readonly HashSet visibleSubs = new HashSet(); + + private static double prevCullTime; + private static Rectangle prevCullArea; + /// + /// Interval at which we force culled entites to be updated, regardless if the camera has moved + /// + private const float CullInterval = 0.25f; + /// + /// 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; + /// + /// Update entity culling when any corner of the view has moved more than this + /// + private const int CullMoveThreshold = 50; + public static void CullEntities(Camera cam) { + Rectangle camView = cam.WorldView; + camView = new Rectangle(camView.X - CullMargin, camView.Y + CullMargin, camView.Width + CullMargin * 2, camView.Height + CullMargin * 2); + + if (Math.Abs(camView.X - prevCullArea.X) < CullMoveThreshold && + Math.Abs(camView.Y - prevCullArea.Y) < CullMoveThreshold && + Math.Abs(camView.Right - prevCullArea.Right) < CullMoveThreshold && + Math.Abs(camView.Bottom - prevCullArea.Bottom) < CullMoveThreshold && + prevCullTime > Timing.TotalTime - CullInterval) + { + return; + } + visibleSubs.Clear(); foreach (Submarine sub in Loaded) { if (Level.Loaded != null && sub.WorldPosition.Y < Level.MaxEntityDepth) { continue; } - int margin = 500; Rectangle worldBorders = new Rectangle( - sub.VisibleBorders.X + (int)sub.WorldPosition.X - margin, - sub.VisibleBorders.Y + (int)sub.WorldPosition.Y + margin, - sub.VisibleBorders.Width + margin * 2, - sub.VisibleBorders.Height + margin * 2); + sub.VisibleBorders.X + (int)sub.WorldPosition.X, + sub.VisibleBorders.Y + (int)sub.WorldPosition.Y, + sub.VisibleBorders.Width, + sub.VisibleBorders.Height); - if (RectsOverlap(worldBorders, cam.WorldView)) + if (RectsOverlap(worldBorders, camView)) { visibleSubs.Add(sub); } @@ -64,16 +70,22 @@ namespace Barotrauma visibleEntities.Clear(); } - Rectangle worldView = cam.WorldView; foreach (MapEntity entity in MapEntity.mapEntityList) { if (entity.Submarine != null) { if (!visibleSubs.Contains(entity.Submarine)) { continue; } } - - if (entity.IsVisible(worldView)) { visibleEntities.Add(entity); } + if (entity.IsVisible(camView)) { visibleEntities.Add(entity); } } + + prevCullArea = camView; + prevCullTime = Timing.TotalTime; + } + + public static void ForceVisibilityRecheck() + { + prevCullTime = 0; } public static void Draw(SpriteBatch spriteBatch, bool editing = false) @@ -148,7 +160,7 @@ namespace Barotrauma { if (predicate != null) { - if (!predicate(e)) continue; + if (!predicate(e)) { continue; } } float drawDepth = structure.GetDrawDepth(); int i = 0; @@ -679,6 +691,22 @@ namespace Barotrauma return GameMain.LightManager.Lights.Count(l => l.CastShadows && !l.IsBackground) - disabledItemLightCount; } + public static Vector2 MouseToWorldGrid(Camera cam, Submarine sub) + { + Vector2 position = PlayerInput.MousePosition; + position = cam.ScreenToWorld(position); + + Vector2 worldGridPos = VectorToWorldGrid(position); + + if (sub != null) + { + worldGridPos.X += sub.Position.X % GridSize.X; + worldGridPos.Y += sub.Position.Y % GridSize.Y; + } + + return worldGridPos; + } + public void ClientReadPosition(IReadMessage msg, float sendingTime) { var posInfo = PhysicsBody.ClientRead(msg, sendingTime, parentDebugName: Info.Name); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index 5e22aaa33..a020b263a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -10,14 +10,15 @@ namespace Barotrauma.Networking { msg.Write((byte)ClientNetObject.CHAT_MESSAGE); msg.Write(NetStateID); - msg.Write((byte)Type); + msg.WriteRangedInteger((int)Type, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); + msg.WriteRangedInteger((int)ChatMode, 0, Enum.GetValues(typeof(ChatMode)).Length - 1); msg.Write(Text); } public static void ClientRead(IReadMessage msg) { UInt16 id = msg.ReadUInt16(); - ChatMessageType type = (ChatMessageType)msg.ReadByte(); + ChatMessageType type = (ChatMessageType)msg.ReadRangedInteger(0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None; string txt = ""; string styleSetting = string.Empty; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 797640696..4409512fb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -1831,7 +1831,8 @@ namespace Barotrauma.Networking GameMain.GameScreen.Select(); - AddChatMessage($"ServerMessage.HowToCommunicate~[chatbutton]={GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Chat)}~[radiobutton]={GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.RadioChat)}", ChatMessageType.Server); + // TODO: Re-enable the server message once it's been edited and translated + //AddChatMessage($"ServerMessage.HowToCommunicate~[chatbutton]={GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Chat)}~[radiobutton]={GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.RadioChat)}", ChatMessageType.Server); yield return CoroutineStatus.Success; } @@ -2503,6 +2504,7 @@ namespace Barotrauma.Networking message, type, gameStarted && myCharacter != null ? myCharacter : null); + chatMessage.ChatMode = GameMain.ActiveChatMode; lastQueueChatMsgID++; chatMessage.NetStateID = lastQueueChatMsgID; @@ -2788,19 +2790,21 @@ namespace Barotrauma.Networking GameMain.GameSession = null; } - public void SendCharacterInfo() + public void SendCharacterInfo(string newName = null) { IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.UPDATE_CHARACTERINFO); - WriteCharacterInfo(msg); + WriteCharacterInfo(msg, newName); msg.Write((byte)ServerNetObject.END_OF_MESSAGE); clientPeer?.Send(msg, DeliveryMethod.Reliable); } - public void WriteCharacterInfo(IWriteMessage msg) + public void WriteCharacterInfo(IWriteMessage msg, string newName = null) { msg.Write(characterInfo == null); - if (characterInfo == null) return; + if (characterInfo == null) { return; } + + msg.Write(newName ?? string.Empty); msg.Write((byte)characterInfo.Head.Preset.TagSet.Count); foreach (Identifier tag in characterInfo.Head.Preset.TagSet) @@ -3284,10 +3288,8 @@ namespace Barotrauma.Networking { if (GUI.KeyboardDispatcher.Subscriber == null) { - bool chatKeyHit = PlayerInput.KeyHit(InputType.Chat); - bool radioKeyHit = PlayerInput.KeyHit(InputType.RadioChat) && (Character.Controlled == null || Character.Controlled.SpeechImpediment < 100); - - if (chatKeyHit || radioKeyHit) + var chatKeyStates = ChatBox.ChatKeyStates.GetChatKeyStates(); + if (chatKeyStates.AnyHit) { if (msgBox.Selected) { @@ -3298,34 +3300,8 @@ namespace Barotrauma.Networking { if (Screen.Selected == GameMain.GameScreen) { - if (chatKeyHit) - { - msgBox.AddToGUIUpdateList(); - ChatBox.GUIFrame.Flash(Color.DarkGreen, 0.5f); - if (!chatBox.ToggleOpen) - { - ChatBox.CloseAfterMessageSent = !ChatBox.ToggleOpen; - ChatBox.ToggleOpen = true; - } - } - - if (radioKeyHit) - { - msgBox.AddToGUIUpdateList(); - ChatBox.GUIFrame.Flash(Color.YellowGreen, 0.5f); - if (!chatBox.ToggleOpen) - { - ChatBox.CloseAfterMessageSent = !ChatBox.ToggleOpen; - ChatBox.ToggleOpen = true; - } - - if (!msgBox.Text.StartsWith(ChatBox.RadioChatString)) - { - msgBox.Text = ChatBox.RadioChatString; - } - } + ChatBox.ApplySelectionInputs(msgBox, false, chatKeyStates); } - msgBox.Select(msgBox.Text.Length); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs index 82ce2130f..1134f3aa0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs @@ -1,4 +1,6 @@ -namespace Barotrauma.Networking +using System; + +namespace Barotrauma.Networking { partial class OrderChatMessage : ChatMessage { @@ -6,7 +8,7 @@ { msg.Write((byte)ClientNetObject.CHAT_MESSAGE); msg.Write(NetStateID); - msg.Write((byte)ChatMessageType.Order); + msg.WriteRangedInteger((int)ChatMessageType.Order, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); WriteOrder(msg); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index cf2caabed..f1c611704 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -227,20 +227,10 @@ namespace Barotrauma.Networking bool allowEnqueue = overrideSound != null; if (GameMain.WindowActive && SettingsMenu.Instance is null) { - ForceLocal = captureTimer > 0 ? ForceLocal : GameSettings.CurrentConfig.Audio.UseLocalVoiceByDefault; - bool pttDown = false; - if ((PlayerInput.KeyDown(InputType.Voice) || PlayerInput.KeyDown(InputType.LocalVoice)) && - GUI.KeyboardDispatcher.Subscriber == null) + bool pttDown = PlayerInput.KeyDown(InputType.Voice) && GUI.KeyboardDispatcher.Subscriber == null; + if (pttDown || captureTimer <= 0) { - pttDown = true; - if (PlayerInput.KeyDown(InputType.LocalVoice)) - { - ForceLocal = true; - } - else - { - ForceLocal = false; - } + ForceLocal = GameMain.ActiveChatMode == ChatMode.Local; } if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Activity) { @@ -257,7 +247,6 @@ namespace Barotrauma.Networking } } } - if (allowEnqueue || captureTimer > 0) { LastEnqueueAudio = DateTime.Now; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index 38cda4a69..7bfeb7275 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -205,7 +205,10 @@ namespace Barotrauma SettingCarouselElement prevDifficulty = difficultyOptions.FirstOrNull(element => element.Value == prevSettings.Difficulty) ?? difficultyOptions[1]; SettingValue difficultyInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("leveldifficulty"), TextManager.Get("leveldifficultyexplanation"), prevDifficulty, verticalSize, difficultyOptions); - SettingValue maxMissionCountInput = CreateGUINumberInputCarousel(settingsList.Content, TextManager.Get("maxmissioncount"), TextManager.Get("maxmissioncounttooltip"), prevSettings.MaxMissionCount, valueStep: 1, verticalSize); + SettingValue maxMissionCountInput = CreateGUINumberInputCarousel(settingsList.Content, TextManager.Get("maxmissioncount"), TextManager.Get("maxmissioncounttooltip"), + prevSettings.MaxMissionCount, + valueStep: 1, minValue: CampaignSettings.MinMissionCountLimit, maxValue: CampaignSettings.MaxMissionCountLimit, + verticalSize); presetDropdown.OnSelected = (selected, o) => { @@ -231,7 +234,7 @@ namespace Barotrauma }; // Create a number input with plus and minus buttons because for some reason the default GUINumberInput buttons don't work when in a GUIMessageBox - static SettingValue CreateGUINumberInputCarousel(GUIComponent parent, LocalizedString description, LocalizedString tooltip, int defaultValue, int valueStep, float verticalSize) + static SettingValue CreateGUINumberInputCarousel(GUIComponent parent, LocalizedString description, LocalizedString tooltip, int defaultValue, int valueStep, int minValue, int maxValue, float verticalSize) { GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, horizontalSize: 0.55f, verticalSize: verticalSize); @@ -243,7 +246,9 @@ namespace Barotrauma GUINumberInput numberInput = new GUINumberInput(new RectTransform(Vector2.One, inputContainer.RectTransform, Anchor.Center), NumberType.Int, textAlignment: Alignment.Center, style: "GUITextBox", hidePlusMinusButtons: true) { - IntValue = defaultValue + IntValue = defaultValue, + MinValueInt = minValue, + MaxValueInt = maxValue }; 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) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 4efe53c5c..f3783a78a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -126,7 +126,7 @@ namespace Barotrauma DrawMap(graphics, spriteBatch, deltaTime); sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("DrawMap", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map", sw.ElapsedTicks); sw.Restart(); spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); @@ -165,7 +165,7 @@ namespace Barotrauma spriteBatch.End(); sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("DrawHUD", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:HUD", sw.ElapsedTicks); sw.Restart(); } @@ -178,6 +178,9 @@ namespace Barotrauma GameMain.ParticleManager.UpdateTransforms(); + Stopwatch sw = new Stopwatch(); + sw.Start(); + GameMain.LightManager.ObstructVision = Character.Controlled != null && Character.Controlled.ObstructVision && @@ -185,6 +188,10 @@ namespace Barotrauma GameMain.LightManager.UpdateObstructVision(graphics, spriteBatch, cam, Character.Controlled?.CursorWorldPosition ?? Vector2.Zero); + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:LOS", sw.ElapsedTicks); + sw.Restart(); + //------------------------------------------------------------------------ graphics.SetRenderTarget(renderTarget); graphics.Clear(Color.Transparent); @@ -196,9 +203,17 @@ namespace Barotrauma Submarine.DrawPaintedColors(spriteBatch, false); spriteBatch.End(); + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:BackStructures", sw.ElapsedTicks); + sw.Restart(); + graphics.SetRenderTarget(null); GameMain.LightManager.RenderLightMap(graphics, spriteBatch, cam, renderTarget); + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:Lighting", sw.ElapsedTicks); + sw.Restart(); + //------------------------------------------------------------------------ graphics.SetRenderTarget(renderTargetBackground); if (Level.Loaded == null) @@ -228,6 +243,10 @@ namespace Barotrauma spriteBatch.Draw(renderTarget, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White); spriteBatch.End(); + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:BackLevel", sw.ElapsedTicks); + sw.Restart(); + //---------------------------------------------------------------------------- //Start drawing to the normal render target (stuff that can't be seen through the LOS effect) @@ -248,6 +267,10 @@ namespace Barotrauma } spriteBatch.End(); + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:BackCharactersItems", sw.ElapsedTicks); + sw.Restart(); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); DrawDeformed(firstPass: true); DrawDeformed(firstPass: false); @@ -266,8 +289,16 @@ namespace Barotrauma } } + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:DeformableCharacters", sw.ElapsedTicks); + sw.Restart(); + Level.Loaded?.DrawFront(spriteBatch, cam); + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:FrontLevel", sw.ElapsedTicks); + sw.Restart(); + //draw the rendertarget and particles that are only supposed to be drawn in water into renderTargetWater graphics.SetRenderTarget(renderTargetWater); @@ -302,6 +333,10 @@ namespace Barotrauma WaterRenderer.Instance.RenderAir(graphics, cam, renderTarget, Cam.ShaderTransform); graphics.DepthStencilState = DepthStencilState.None; + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:FrontParticles", sw.ElapsedTicks); + sw.Restart(); + spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied, SamplerState.LinearWrap, null, null, @@ -310,10 +345,18 @@ namespace Barotrauma Submarine.DrawDamageable(spriteBatch, damageEffect, false); spriteBatch.End(); + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:FrontDamageable", sw.ElapsedTicks); + sw.Restart(); + spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); Submarine.DrawFront(spriteBatch, false, null); spriteBatch.End(); + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:FrontStructuresItems", sw.ElapsedTicks); + sw.Restart(); + //draw additive particles that are inside a sub spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, null, DepthStencilState.Default, null, null, cam.Transform); GameMain.ParticleManager.Draw(spriteBatch, true, true, Particles.ParticleBlendState.Additive); @@ -349,6 +392,10 @@ namespace Barotrauma } spriteBatch.End(); + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:FrontMisc", sw.ElapsedTicks); + sw.Restart(); + if (GameMain.LightManager.LosEnabled && GameMain.LightManager.LosMode != LosMode.None && Lights.LightManager.ViewTarget != null) { GameMain.LightManager.LosEffect.CurrentTechnique = GameMain.LightManager.LosEffect.Techniques["LosShader"]; @@ -457,6 +504,10 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.Lerp(Color.TransparentBlack, Color.Black, fadeToBlackState), isFilled: true); spriteBatch.End(); } + + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:PostProcess", sw.ElapsedTicks); + sw.Restart(); } partial void UpdateProjSpecific(double deltaTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 1c21c6f27..4bf99f6d8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -110,7 +110,7 @@ namespace Barotrauma public bool CampaignCharacterDiscarded { get; - private set; + set; } //elements that can only be used by the host @@ -1253,9 +1253,6 @@ namespace Barotrauma CharacterAppearanceCustomizationMenu?.Dispose(); JobSelectionFrame = null; - - /*foreach (Sprite sprite in jobPreferenceSprites) { sprite.Remove(); } - jobPreferenceSprites.Clear();*/ } public override void Select() @@ -1417,7 +1414,7 @@ namespace Barotrauma characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, GameMain.Client.Name, null); characterInfo.RecreateHead(MultiplayerPreferences.Instance); GameMain.Client.CharacterInfo = characterInfo; - characterInfo.OmitJobInPortraitClothing = false; + characterInfo.OmitJobInMenus = true; } parent.ClearChildren(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index a702a3d4d..442ea1bae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -1870,7 +1870,8 @@ namespace Barotrauma } addSubAndSaveModProject(modProject, savePath, fileListPath); } - else if (MainSub?.Info != null + else if (MainSub?.Info?.FilePath != null + && MainSub.Info.Name != null && MainSub.Info.FilePath.StartsWith(ContentPackage.LocalModsDir) && MainSub.Info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) { @@ -2321,7 +2322,7 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), beaconMinDifficultyGroup.RectTransform), TextManager.Get("minleveldifficulty"), textAlignment: Alignment.CenterLeft, wrap: true); - 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), beaconMinDifficultyGroup.RectTransform), NumberType.Int) { IntValue = (int)(MainSub?.Info?.BeaconStationInfo?.MinLevelDifficulty ?? 0), MinValueInt = 0, @@ -2331,13 +2332,14 @@ namespace Barotrauma MainSub.Info.BeaconStationInfo.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) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), beaconMaxDifficultyGroup.RectTransform), TextManager.Get("maxleveldifficulty"), textAlignment: Alignment.CenterLeft, wrap: true); - 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), beaconMaxDifficultyGroup.RectTransform), NumberType.Int) { IntValue = (int)(MainSub?.Info?.BeaconStationInfo?.MaxLevelDifficulty ?? 100), MinValueInt = 0, @@ -2347,7 +2349,7 @@ namespace Barotrauma MainSub.Info.BeaconStationInfo.MaxLevelDifficulty = numberInput.IntValue; } }; - + beaconMaxDifficultyGroup.RectTransform.MaxSize = numInput.TextBox.RectTransform.MaxSize; new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), TextManager.Get("allowdamagedwalls")) { Selected = MainSub?.Info?.BeaconStationInfo?.AllowDamagedWalls ?? true, @@ -2545,6 +2547,10 @@ namespace Barotrauma { MainSub.Info.OutpostModuleInfo ??= new OutpostModuleInfo(MainSub.Info); } + else if (type == SubmarineType.BeaconStation) + { + MainSub.Info.BeaconStationInfo ??= new BeaconStationInfo(MainSub.Info); + } previewImageButtonHolder.Children.ForEach(c => c.Enabled = type != SubmarineType.OutpostModule); outpostSettingsContainer.Visible = type == SubmarineType.OutpostModule; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index 45c0e1c6a..d514f6129 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -1,6 +1,7 @@ #nullable enable using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; using Barotrauma.Extensions; @@ -36,6 +37,8 @@ namespace Barotrauma public readonly WorkshopMenu WorkshopMenu; + private static readonly ImmutableHashSet LegacyInputTypes = new List() { InputType.Chat, InputType.RadioChat }.ToImmutableHashSet(); + public static SettingsMenu Create(RectTransform mainParent) { Instance?.Close(); @@ -392,6 +395,9 @@ namespace Barotrauma Label(audio, TextManager.Get("MusicVolume"), GUIStyle.SubHeadingFont); Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.MusicVolume, (v) => unsavedConfig.Audio.MusicVolume = v); + Label(audio, TextManager.Get("UiSoundVolume"), GUIStyle.SubHeadingFont); + Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.UiVolume, (v) => unsavedConfig.Audio.UiVolume = v); + Tickbox(audio, TextManager.Get("MuteOnFocusLost"), TextManager.Get("MuteOnFocusLostTooltip"), unsavedConfig.Audio.MuteOnFocusLost, (v) => unsavedConfig.Audio.MuteOnFocusLost = v); Tickbox(audio, TextManager.Get("DynamicRangeCompression"), TextManager.Get("DynamicRangeCompressionTooltip"), unsavedConfig.Audio.DynamicRangeCompressionEnabled, (v) => unsavedConfig.Audio.DynamicRangeCompressionEnabled = v); Spacer(audio); @@ -481,10 +487,14 @@ namespace Barotrauma HashSet inputButtons = new HashSet(); Action? currentSetter = null; - void addInputToRow(GUILayoutGroup currRow, LocalizedString labelText, Func valueNameGetter, Action valueSetter) + void addInputToRow(GUILayoutGroup currRow, LocalizedString labelText, Func valueNameGetter, Action valueSetter, bool isLegacyBind = false) { var inputFrame = new GUIFrame(new RectTransform((0.5f, 1.0f), currRow.RectTransform), style: null); + if (isLegacyBind) + { + labelText = TextManager.GetWithVariable("legacyitemformat", "[name]", labelText); + } var label = new GUITextBlock(new RectTransform((0.6f, 1.0f), inputFrame.RectTransform), labelText, font: GUIStyle.SmallFont) {ForceUpperCase = ForceUpperCase.Yes}; var inputBox = new GUIButton( @@ -515,6 +525,12 @@ namespace Barotrauma return true; } }; + if (isLegacyBind) + { + label.TextColor = Color.Lerp(label.TextColor, label.DisabledTextColor, 0.5f); + inputBox.Color = Color.Lerp(inputBox.Color, inputBox.DisabledColor, 0.5f); + inputBox.TextColor = Color.Lerp(inputBox.TextColor, label.DisabledTextColor, 0.5f); + } inputButtons.Add(inputBox); } @@ -594,7 +610,8 @@ namespace Barotrauma currRow, TextManager.Get($"InputType.{input}"), () => unsavedConfig.KeyMap.Bindings[input].Name, - (v) => unsavedConfig.KeyMap = unsavedConfig.KeyMap.WithBinding(input, v)); + (v) => unsavedConfig.KeyMap = unsavedConfig.KeyMap.WithBinding(input, v), + LegacyInputTypes.Contains(input)); } } @@ -609,30 +626,29 @@ namespace Barotrauma var input = unsavedConfig.InventoryKeyMap.Bindings[currIndex]; addInputToRow( currRow, - TextManager.GetWithVariable("inventoryslotkeybind", "[slotnumber]", (currIndex+1).ToString(CultureInfo.InvariantCulture)), + TextManager.GetWithVariable("inventoryslotkeybind", "[slotnumber]", (currIndex + 1).ToString(CultureInfo.InvariantCulture)), () => unsavedConfig.InventoryKeyMap.Bindings[currIndex].Name, (v) => unsavedConfig.InventoryKeyMap = unsavedConfig.InventoryKeyMap.WithBinding(currIndex, v)); } } GUILayoutGroup resetControlsHolder = - new GUILayoutGroup(new RectTransform((1.75f, 0.1f), layout.RectTransform), isHorizontal: true) + new GUILayoutGroup(new RectTransform((1.75f, 0.1f), layout.RectTransform), isHorizontal: true, childAnchor: Anchor.Center) { RelativeSpacing = 0.1f }; var defaultBindingsButton = new GUIButton(new RectTransform(new Vector2(0.45f, 1.0f), resetControlsHolder.RectTransform), - TextManager.Get("SetDefaultBindings"), style: "GUIButtonSmall") + TextManager.Get("Reset"), style: "GUIButtonSmall") { - ToolTip = TextManager.Get("SetDefaultBindingsTooltip") - }; - - var legacyBindingsButton = - new GUIButton(new RectTransform(new Vector2(0.45f, 1.0f), resetControlsHolder.RectTransform), - TextManager.Get("SetLegacyBindings"), style: "GUIButtonSmall") - { - ToolTip = TextManager.Get("SetLegacyBindingsTooltip") + ToolTip = TextManager.Get("SetDefaultBindingsTooltip"), + OnClicked = (btn, userdata) => + { + unsavedConfig.InventoryKeyMap = GameSettings.Config.InventoryKeyMapping.GetDefault(); + unsavedConfig.KeyMap = GameSettings.Config.KeyMapping.GetDefault(); + return true; + } }; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index 7fe005bb5..a2418f455 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -705,7 +705,7 @@ namespace Barotrauma.Sounds public void ApplySettings() { SetCategoryGainMultiplier("default", GameSettings.CurrentConfig.Audio.SoundVolume, 0); - SetCategoryGainMultiplier("ui", GameSettings.CurrentConfig.Audio.SoundVolume, 0); + SetCategoryGainMultiplier("ui", GameSettings.CurrentConfig.Audio.UiVolume, 0); SetCategoryGainMultiplier("waterambience", GameSettings.CurrentConfig.Audio.SoundVolume, 0); SetCategoryGainMultiplier("music", GameSettings.CurrentConfig.Audio.MusicVolume, 0); SetCategoryGainMultiplier("voip", Math.Min(GameSettings.CurrentConfig.Audio.VoiceChatVolume, 1.0f), 0); diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 2763d0211..c0771a588 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.4.0 + 0.18.5.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index d129a0104..46e1ecfdf 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.4.0 + 0.18.5.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 1f520e768..3fdf81956 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.4.0 + 0.18.5.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 77e6d7d8a..865d79238 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.4.0 + 0.18.5.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index c68077f24..0e97bc036 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.4.0 + 0.18.5.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index 9d64d3876..075a0bd30 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -10,7 +10,8 @@ namespace Barotrauma.Networking c.KickAFKTimer = 0.0f; UInt16 ID = msg.ReadUInt16(); - ChatMessageType type = (ChatMessageType)msg.ReadByte(); + ChatMessageType type = (ChatMessageType)msg.ReadRangedInteger(0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); + ChatMode chatMode = (ChatMode)msg.ReadRangedInteger(0, Enum.GetValues(typeof(ChatMode)).Length - 1); string txt; Character orderTargetCharacter = null; @@ -172,7 +173,7 @@ namespace Barotrauma.Networking } else { - GameMain.Server.SendChatMessage(txt, null, c); + GameMain.Server.SendChatMessage(txt, senderClient: c, chatMode: chatMode); } } @@ -203,7 +204,7 @@ namespace Barotrauma.Networking { msg.Write((byte)ServerNetObject.CHAT_MESSAGE); msg.Write(NetStateID); - msg.Write((byte)Type); + msg.WriteRangedInteger((int)Type, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); msg.Write((byte)ChangeType); msg.Write(Text); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 234e94be4..bc9ae28ab 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -2696,6 +2696,24 @@ namespace Barotrauma.Networking if (newName == c.Name) { return false; } + if (IsNameValid(c, newName)) + { + string oldName = c.Name; + c.Name = newName; + c.Connection.Name = newName; + SendChatMessage($"ServerMessage.NameChangeSuccessful~[oldname]={oldName}~[newname]={newName}", ChatMessageType.Server); + return true; + } + else + { + return false; + } + } + + private bool IsNameValid(Client c, string newName) + { + newName = Client.SanitizeName(newName); + if (c.Connection != OwnerConnection) { if (!Client.IsValidName(newName, serverSettings)) @@ -2723,9 +2741,6 @@ namespace Barotrauma.Networking return false; } - SendChatMessage($"ServerMessage.NameChangeSuccessful~[oldname]={c.Name}~[newname]={newName}", ChatMessageType.Server); - c.Name = newName; - c.Connection.Name = newName; return true; } @@ -2967,7 +2982,7 @@ namespace Barotrauma.Networking /// /// Add the message to the chatbox and pass it to all clients who can receive it /// - public void SendChatMessage(string message, ChatMessageType? type = null, Client senderClient = null, Character senderCharacter = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None) + public void SendChatMessage(string message, ChatMessageType? type = null, Client senderClient = null, Character senderCharacter = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None, ChatMode chatMode = ChatMode.None) { string senderName = ""; @@ -3023,6 +3038,10 @@ namespace Barotrauma.Networking type = ChatMessageType.Private; } + else if (chatMode == ChatMode.Radio) + { + type = ChatMessageType.Radio; + } else { type = ChatMessageType.Default; @@ -3051,7 +3070,6 @@ namespace Barotrauma.Networking { senderCharacter = senderClient.Character; senderName = senderCharacter == null ? senderClient.Name : senderCharacter.Name; - if (type == ChatMessageType.Private) { if (senderCharacter != null && !senderCharacter.IsDead || targetClient.Character != null && !targetClient.Character.IsDead) @@ -3512,6 +3530,20 @@ namespace Barotrauma.Networking return; } + string newName = message.ReadString(); + if (string.IsNullOrEmpty(newName)) + { + newName = sender.Name; + } + else + { + newName = Client.SanitizeName(newName); + if (!IsNameValid(sender, newName)) + { + newName = sender.Name; + } + } + int tagCount = message.ReadByte(); HashSet tagSet = new HashSet(); for (int i = 0; i < tagCount; i++) @@ -3539,7 +3571,7 @@ namespace Barotrauma.Networking } } - sender.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, sender.Name); + sender.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, newName); sender.CharacterInfo.RecreateHead(tagSet.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); sender.CharacterInfo.Head.SkinColor = skinColor; sender.CharacterInfo.Head.HairColor = hairColor; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs index f2cef0e0e..d8d509a43 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs @@ -1,4 +1,6 @@ -namespace Barotrauma.Networking +using System; + +namespace Barotrauma.Networking { partial class OrderChatMessage : ChatMessage { @@ -6,7 +8,7 @@ { msg.Write((byte)ServerNetObject.CHAT_MESSAGE); msg.Write(NetStateID); - msg.Write((byte)ChatMessageType.Order); + msg.WriteRangedInteger((int)ChatMessageType.Order, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); msg.Write(SenderName); msg.Write(SenderClient != null); if (SenderClient != null) diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 613378097..f9f448b02 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.4.0 + 0.18.5.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml index a80fcbe81..d88b556aa 100644 --- a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml +++ b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml @@ -2,8 +2,8 @@ - - + + () is { } pickable && !pickable.IsAttached, "Invalid target in AIObjectiveCleanUpItems - the the objective should only be checking pickable, non-attached items."); + System.Diagnostics.Debug.Assert(target.Prefab.PreferredContainers.Any(), "Invalid target in AIObjectiveCleanUpItems - the the objective should only be checking items that have preferred containers defined."); + // 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) && IsItemInsideValidSubmarine(target, character); } @@ -57,7 +59,7 @@ namespace Barotrauma return true; } - protected override IEnumerable GetList() => Item.ItemList; + protected override IEnumerable GetList() => Item.CleanableItems; protected override AIObjective ObjectiveConstructor(Item item) => new AIObjectiveCleanupItem(item, character, objectiveManager, priorityModifier: PriorityModifier) @@ -102,9 +104,6 @@ namespace Barotrauma } if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; } if (item.HasBallastFloraInHull) { return false; } - var pickable = item.GetComponent(); - if (pickable == null) { return false; } - if (pickable is Holdable h && h.Attachable && h.Attached) { return false; } var wire = item.GetComponent(); if (wire != null) { @@ -118,10 +117,6 @@ namespace Barotrauma return false; } } - if (item.Prefab.PreferredContainers.None()) - { - return false; - } if (!checkInventory) { return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index a63159e19..5b5665bf9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -488,7 +488,7 @@ namespace Barotrauma if (hull != null) { itemsToClean.Clear(); - foreach (Item item in Item.ItemList) + foreach (Item item in Item.CleanableItems) { if (item.CurrentHull != hull) { continue; } if (AIObjectiveCleanupItems.IsValidTarget(item, character, checkInventory: true, allowUnloading: false) && !ignoredItems.Contains(item)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs index b13723512..a5b9338dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs @@ -39,12 +39,21 @@ namespace Barotrauma { TargetContainers.Add(targetContainer); } + else + { + foreach (Item item in Item.ItemList) + { + if (!OrderPrefab.TargetItemsMatchItem(TargetContainerTags, item)) { continue; } + TargetContainers.Add(item); + } + } TargetCondition = option == "turretammo" ? ItemCondition.Empty : ItemCondition.Full; } protected override bool Filter(Item target) { - if (!IsValidTarget(target, character, TargetContainerTags, TargetCondition)) { return false; } + //don't pass TargetContainerTags to the method (no need to filter by tags anymore, it's already done when populating TargetContainers) + if (!IsValidTarget(target, character, null, TargetCondition)) { return false; } if (target.CurrentHull == null || target.CurrentHull.FireSources.Count > 0) { return false; } if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; } return true; @@ -52,8 +61,7 @@ namespace Barotrauma public static bool IsValidTarget(Item item, Character character, ImmutableArray? targetContainerTags = null, ItemCondition? targetCondition = null) { - if (item == null) { return false; } - if (item.Removed) { return false; } + if (item == null || item.Removed) { return false; } if (targetContainerTags.HasValue && !OrderPrefab.TargetItemsMatchItem(targetContainerTags.Value, item)) { return false; } if (!(item.GetComponent() is ItemContainer container)) { return false; } if (container.Inventory == null) { return false; } @@ -88,7 +96,7 @@ namespace Barotrauma } } - protected override IEnumerable GetList() => TargetContainers.Any() ? TargetContainers : Item.ItemList; + protected override IEnumerable GetList() => TargetContainers; protected override AIObjective ObjectiveConstructor(Item target) => new AIObjectiveLoadItem(target, TargetContainerTags, TargetCondition, Option, character, objectiveManager, PriorityModifier); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs index c908ac461..6377ef8a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -13,7 +13,7 @@ namespace Barotrauma public override bool KeepDivingGearOn => true; public override bool AllowAutomaticItemUnequipping => true; - private IEnumerable pumpList; + private List pumpList; public AIObjectivePumpWater(Character character, AIObjectiveManager objectiveManager, Identifier option, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier, option) { } @@ -26,13 +26,8 @@ namespace Barotrauma protected override bool Filter(Pump pump) { - if (pump == null) { return false; } if (pump.Item.IgnoreByAI(character)) { return false; } if (!pump.Item.IsInteractable(character)) { return false; } - if (pump.Item.HasTag("ballast")) { return false; } - if (pump.Item.Submarine == null) { return false; } - if (pump.Item.CurrentHull == null) { return false; } - if (pump.Item.Submarine.TeamID != character.TeamID) { return false; } if (pump.IsAutoControlled) { return false; } if (pump.Item.ConditionPercentage <= 0) { return false; } if (pump.Item.CurrentHull.FireSources.Count > 0) { return false; } @@ -50,7 +45,16 @@ namespace Barotrauma if (pumpList == null) { if (character == null || character.Submarine == null) { return Array.Empty(); } - pumpList = character.Submarine.GetItems(true).Select(i => i.GetComponent()).Where(p => p != null); + + pumpList = new List(); + foreach (Item item in character.Submarine.GetItems(true)) + { + var pump = item.GetComponent(); + if (pump == null || pump.Item.Submarine == null || pump.Item.CurrentHull == null) { continue; } + if (pump.Item.Submarine.TeamID != character.TeamID) { continue; } + if (pump.Item.HasTag("ballast")) { continue; } + pumpList.Add(pump); + } } return pumpList; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index 005e3aa44..90578f6b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -136,7 +136,7 @@ namespace Barotrauma return MathHelper.Lerp(0, 100, MathHelper.Clamp(damagePriority * successFactor, 0, 1)); } - protected override IEnumerable GetList() => Item.ItemList; + protected override IEnumerable GetList() => Item.RepairableItems; protected override AIObjective ObjectiveConstructor(Item item) => new AIObjectiveRepairItem(character, item, objectiveManager, priorityModifier: PriorityModifier, isPriority: item == PrioritizedItem); @@ -156,6 +156,9 @@ namespace Barotrauma if (character.IsOnPlayerTeam && item.Submarine.Info.IsOutpost) { return false; } if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { return false; } if (item.Repairables.None()) { return false; } + + System.Diagnostics.Debug.Assert(item.Repairables.Any(), "Invalid target in AIObjectiveRepairItems - the objective should only be checking items that have a Repairable component (Item.RepairableItems)"); + return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 22f35f724..49d36bdfa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -356,7 +356,10 @@ namespace Barotrauma } } - public bool OmitJobInPortraitClothing; + /// + /// Can be used to disable displaying the job in any info panels + /// + public bool OmitJobInMenus; private Sprite portrait; public Sprite Portrait @@ -434,7 +437,7 @@ namespace Barotrauma { if (attachmentSprites == null) { - LoadAttachmentSprites(OmitJobInPortraitClothing); + LoadAttachmentSprites(); } return attachmentSprites; } @@ -1092,7 +1095,7 @@ namespace Barotrauma private static IEnumerable GetWeights(IEnumerable elements) => elements.Select(h => h.GetAttributeFloat("commonness", 1f)); - partial void LoadAttachmentSprites(bool omitJob); + partial void LoadAttachmentSprites(); private int CalculateSalary() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index f8ea728fd..8153bca07 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -79,7 +79,6 @@ namespace Barotrauma /// public static IReadOnlyDictionary ItemRepairPriorities => _itemRepairPriorities; - public static ContentXElement NoJobElement; public static JobPrefab Get(string identifier) { if (Prefabs.ContainsKey(identifier)) @@ -213,7 +212,7 @@ namespace Barotrauma public SkillPrefab PrimarySkill => Skills?.FirstOrDefault(s => s.IsPrimarySkill); public ContentXElement Element { get; private set; } - public ContentXElement ClothingElement { get; private set; } + public int Variants { get; private set; } public JobPrefab(ContentXElement element, JobsFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) @@ -288,9 +287,6 @@ namespace Barotrauma Variants = variant; Skills.Sort((x,y) => y.LevelRange.Start.CompareTo(x.LevelRange.Start)); - - // Disabled on purpose, TODO: remove all references? - //ClothingElement = element.GetChildElement("PortraitClothing"); } public static JobPrefab Random(Rand.RandSync sync, Func predicate = null) => Prefabs.GetRandom(p => !p.HiddenJob && (predicate == null || predicate(p)), sync); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index 4638671f3..d8f8992b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -121,7 +121,7 @@ namespace Barotrauma return folder.CleanUpPathCrossPlatform(correctFilenameCase: true); } - public static T GetDefaultRagdollParams(Identifier speciesName) where T : RagdollParams, new() => GetRagdollParams(speciesName, GetDefaultFileName(speciesName)); + 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! diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/JobsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/JobsFile.cs index 7c2291e84..3fa28a615 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/JobsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/JobsFile.cs @@ -22,11 +22,7 @@ namespace Barotrauma { foreach (var element in mainElement.Elements()) { - if (element.NameAsIdentifier() == "nojob") - { - JobPrefab.NoJobElement ??= element; - } - else if (element.NameAsIdentifier() == "ItemRepairPriorities") + if (element.NameAsIdentifier() == "ItemRepairPriorities") { foreach (var subElement in element.Elements()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 8ef199011..bf8d1addd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -177,4 +177,11 @@ namespace Barotrauma Int, Float } + + public enum ChatMode + { + None, + Local, + Radio + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 717951a59..ea2e4ed68 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -1028,13 +1028,14 @@ namespace Barotrauma var itemsToTransfer = new List<(Item item, Item container)>(); if (PendingSubmarineSwitch != null) { + var connectedSubs = currentSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player).ToHashSet(); // Remove items from the old sub foreach (Item item in Item.ItemList) { if (item.Removed) { continue; } if (item.NonInteractable) { continue; } if (item.HiddenInGame) { continue; } - if (item.Submarine != currentSub) { continue; } + if (!connectedSubs.Contains(item.Submarine)) { continue; } if (item.Prefab.DontTransferBetweenSubs) { continue; } if (item.GetRootInventoryOwner() is Character) { continue; } if (item.GetComponent() == null && item.GetComponent() == null && item.GetComponent() == null) { continue; } @@ -1058,9 +1059,10 @@ namespace Barotrauma { // Load the new sub var newSub = new Submarine(PendingSubmarineSwitch); + var connectedSubs = newSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player).ToHashSet(); // Move the transferred items List availableContainers = Item.ItemList - .Where(it => it.Submarine == newSub && it.HasTag("crate") && !it.NonInteractable && !it.HiddenInGame && !it.Removed) + .Where(it => connectedSubs.Contains(it.Submarine) && it.HasTag("crate") && !it.NonInteractable && !it.HiddenInGame && !it.Removed) .Select(it => it.GetComponent()) .Where(c => c != null) .ToList(); @@ -1070,7 +1072,7 @@ namespace Barotrauma item.Submarine = newSub; if (item.Container == null) { - newContainer = newSub.FindContainerFor(item, onlyPrimary: true, checkTransferConditions: true); + newContainer = newSub.FindContainerFor(item, onlyPrimary: true, checkTransferConditions: true, allowConnectedSubs: true); } if (item.Container == null && (newContainer == null || !newContainer.OwnInventory.TryPutItem(item, user: null, createNetworkEvent: false))) { @@ -1086,7 +1088,8 @@ namespace Barotrauma var cargoContainer = CargoManager.GetOrCreateCargoContainerFor(item.Prefab, spawnHull, ref availableContainers); if (cargoContainer == null || !cargoContainer.Inventory.TryPutItem(item, user: null, createNetworkEvent: false)) { - item.SetTransform(wp.SimPosition, 0.0f, findNewHull: false, setPrevTransform: false); + Vector2 simPos = ConvertUnits.ToSimUnits(CargoManager.GetCargoPos(spawnHull, item.Prefab)); + item.SetTransform(simPos, 0.0f, findNewHull: false, setPrevTransform: false); } } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignModePresets.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignModePresets.cs index 430bb063a..fd5297d89 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignModePresets.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignModePresets.cs @@ -71,12 +71,22 @@ namespace Barotrauma public float GetFloat(Identifier identifier) { - return values.TryGetValue(identifier, out Either value) && value.TryGet(out float range) ? range : 0.0f; + 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) { - return values.TryGetValue(identifier, out Either value) && value.TryGet(out int integer) ? integer : 0; + 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; } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs index 493ef435d..3e792f98d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs @@ -13,13 +13,14 @@ namespace Barotrauma SelectNextCharacter, SelectPreviousCharacter, Voice, - LocalVoice, Deselect, Shoot, Command, TakeOneFromInventorySlot, TakeHalfFromInventorySlot, NextFireMode, - PreviousFireMode + PreviousFireMode, + ActiveChat, + ToggleChatMode, } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index bb9c5bb64..eca3653c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -270,7 +270,11 @@ namespace Barotrauma.Items.Components { Body?.SetTransform(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f); } - + if (linkedGap != null) + { + RefreshLinkedGap(); + linkedGap.Rect = item.Rect; + } #if CLIENT UpdateConvexHulls(); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 9538b5805..0fbda966d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -41,6 +41,8 @@ namespace Barotrauma.Items.Components private Character prevEquipper; + public override bool IsAttached => Attached; + private bool attachable, attached, attachedByDefault; private Voronoi2.VoronoiCell attachTargetCell; private PhysicsBody body; @@ -71,6 +73,7 @@ namespace Barotrauma.Items.Components set { attached = value; + item.CheckCleanable(); item.SetActiveSprite(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index f1651b408..5086b6899 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -4,7 +4,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -20,6 +19,8 @@ namespace Barotrauma.Items.Components private CoroutineHandle pickingCoroutine; + public virtual bool IsAttached => false; + public List AllowedSlots { get { return allowedSlots; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index c615ae481..f7b360dad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -94,7 +94,7 @@ namespace Barotrauma.Items.Components get; set; } - [Serialize(true, IsPropertySaveable.No, description: "Can the item hit broken doors.")] + [Serialize(true, IsPropertySaveable.No, description: "Can the item hit doors.")] public bool HitItems { get; set; } [Serialize(false, IsPropertySaveable.No, description: "Can the item hit broken doors.")] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index e179980be..58050a7ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -187,7 +187,7 @@ namespace Barotrauma.Items.Components [Serialize(false, IsPropertySaveable.No)] public bool RemoveContainedItemsOnDeconstruct { get; set; } - private readonly ImmutableArray slotRestrictions; + private ImmutableArray slotRestrictions; readonly List targets = new List(); @@ -215,13 +215,21 @@ namespace Barotrauma.Items.Components public override bool RecreateGUIOnResolutionChange => true; - public List ContainableItems { get; } + public List ContainableItems { get; private set; } public ItemContainer(Item item, ContentXElement element) : base(item, element) + { + LoadContainableRestrictions(element); + InitProjSpecific(element); + } + + public void LoadContainableRestrictions(ContentXElement element) { int totalCapacity = capacity; + ContainableItems?.Clear(); + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -242,7 +250,7 @@ namespace Barotrauma.Items.Components } } Inventory = new ItemInventory(item, this, totalCapacity, SlotsPerRow); - + List newSlotRestrictions = new List(totalCapacity); for (int i = 0; i < capacity; i++) { @@ -253,7 +261,7 @@ namespace Barotrauma.Items.Components foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().ToLowerInvariant() != "subcontainer") { continue; } - + int subCapacity = subElement.GetAttributeInt("capacity", 1); int subMaxStackSize = subElement.GetAttributeInt("maxstacksize", maxStackSize); @@ -281,7 +289,6 @@ namespace Barotrauma.Items.Components capacity = totalCapacity; slotRestrictions = newSlotRestrictions.ToImmutableArray(); System.Diagnostics.Debug.Assert(totalCapacity == slotRestrictions.Length); - InitProjSpecific(element); } public int GetMaxStackSize(int slotIndex) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index adda6abc2..84ad8903e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -114,6 +114,20 @@ namespace Barotrauma.Items.Components private set; } = true; + [Serialize(false, IsPropertySaveable.No)] + public bool NonInteractableWhenFlippedX + { + get; + set; + } + + [Serialize(false, IsPropertySaveable.No)] + public bool NonInteractableWhenFlippedY + { + get; + set; + } + public Controller(Item item, ContentXElement element) : base(item, element) { @@ -571,6 +585,18 @@ namespace Barotrauma.Items.Components } } + public override void OnItemLoaded() + { + if (item.FlippedX && NonInteractableWhenFlippedX) + { + item.NonInteractable = true; + } + else if (item.FlippedY && NonInteractableWhenFlippedY) + { + item.NonInteractable = true; + } + } + public override void Reset() { base.Reset(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index cae59479d..79e8295cf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -435,7 +435,7 @@ namespace Barotrauma.Items.Components #if CLIENT sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("GridUpdate", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:Power", sw.ElapsedTicks); sw.Restart(); #endif @@ -592,7 +592,7 @@ namespace Barotrauma.Items.Components #if CLIENT sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("PowerUpdate", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:Power", sw.ElapsedTicks); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index ee23c7f90..ae4b5b1c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -17,7 +17,6 @@ namespace Barotrauma Beard, Moustache, FaceAttachment, - JobIndicator, Husk, Herpes } @@ -128,7 +127,6 @@ namespace Barotrauma case WearableType.Beard: case WearableType.Moustache: case WearableType.FaceAttachment: - case WearableType.JobIndicator: case WearableType.Husk: case WearableType.Herpes: Limb = LimbType.Head; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index c7c0b2425..66bc3c1ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -29,6 +29,20 @@ namespace Barotrauma public static IReadOnlyCollection DangerousItems { get { return dangerousItems; } } + private static readonly List repairableItems = new List(); + + /// + /// Items that have one more more Repairable component + /// + public static IReadOnlyCollection RepairableItems => repairableItems; + + private static readonly List cleanableItems = new List(); + + /// + /// Items that may potentially need to be cleaned up (pickable, not attached to a wall, and not inside a valid container) + /// + public static IReadOnlyCollection CleanableItems => cleanableItems; + public new ItemPrefab Prefab => base.Prefab as ItemPrefab; public static bool ShowLinks = true; @@ -186,6 +200,7 @@ namespace Barotrauma if (value != container) { container = value; + CheckCleanable(); SetActiveSprite(); } } @@ -1009,10 +1024,9 @@ namespace Barotrauma InsertToList(); ItemList.Add(this); - if (Prefab.IsDangerous) - { - dangerousItems.Add(this); - } + if (Prefab.IsDangerous) { dangerousItems.Add(this); } + if (Repairables.Any()) { repairableItems.Add(this); } + CheckCleanable(); DebugConsole.Log("Created " + Name + " (" + ID + ")"); @@ -1022,6 +1036,9 @@ namespace Barotrauma ApplyStatusEffects(ActionType.OnSpawn, 1.0f); Components.ForEach(c => c.ApplyStatusEffects(ActionType.OnSpawn, 1.0f)); RecalculateConditionValues(); +#if CLIENT + Submarine.ForceVisibilityRecheck(); +#endif } partial void InitProjSpecific(); @@ -1150,6 +1167,7 @@ namespace Barotrauma drawableComponents.Add(drawable); hasComponentsToDraw = true; #if CLIENT + Submarine.ForceVisibilityRecheck(); cachedVisibleExtents = null; #endif } @@ -1281,6 +1299,27 @@ namespace Barotrauma partial void SetActiveSpriteProjSpecific(); + /// + /// Recheck if the item needs to be included in the list of cleanable items + /// + public void CheckCleanable() + { + var pickable = GetComponent(); + if (pickable != null && !pickable.IsAttached && + Prefab.PreferredContainers.Any() && + (container == null || container.HasTag("allowcleanup"))) + { + if (!cleanableItems.Contains(this)) + { + cleanableItems.Add(this); + } + } + else + { + cleanableItems.Remove(this); + } + } + public override void Move(Vector2 amount, bool ignoreContacts = false) { if (!MathUtils.IsValid(amount)) @@ -2526,7 +2565,7 @@ namespace Barotrauma ic.WasUsed = true; #if CLIENT ic.PlaySound(ActionType.OnUse, character); -#endif +#endif ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb); if (ic.DeleteOnUse) { remove = true; } @@ -2704,6 +2743,9 @@ namespace Barotrauma } SetContainedItemPositions(); +#if CLIENT + Submarine.ForceVisibilityRecheck(); +#endif } public void Equip(Character character) @@ -3368,8 +3410,7 @@ namespace Barotrauma { ic.ShallowRemove(); } - ItemList.Remove(this); - dangerousItems.Remove(this); + RemoveFromLists(); if (body != null) { @@ -3427,8 +3468,8 @@ namespace Barotrauma ic.GuiFrame = null; #endif } - ItemList.Remove(this); - dangerousItems.Remove(this); + + RemoveFromLists(); if (body != null) { @@ -3461,6 +3502,14 @@ namespace Barotrauma RemoveProjSpecific(); } + private void RemoveFromLists() + { + ItemList.Remove(this); + dangerousItems.Remove(this); + repairableItems.Remove(this); + cleanableItems.Remove(this); + } + partial void RemoveProjSpecific(); public static void RemoveByPrefab(ItemPrefab prefab) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index b5fd3d37c..ed95cba99 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -463,6 +463,15 @@ namespace Barotrauma.MapCreatures.Behavior } } + if (root == null) + { + Branches.ForEach(b => b.DisconnectedFromRoot = true); + } + else + { + CheckDisconnectedFromRoot(); + } + void LoadBranch(XElement branchElement, IdRemap idRemap) { Vector2 pos = branchElement.GetAttributeVector2("pos", Vector2.Zero); @@ -649,9 +658,10 @@ namespace Barotrauma.MapCreatures.Behavior toBeRemoved.Clear(); foreach (BallastFloraBranch branch in Branches) { - if (branch.ParentBranch != null && (branch.ParentBranch.DisconnectedFromRoot || branch.ParentBranch.Health <= 0.0f)) + if (branch.ParentBranch == null || branch.ParentBranch.DisconnectedFromRoot || branch.ParentBranch.Health <= 0.0f) { - float speed = MathHelper.Lerp(5.0f, 0.1f, branch.ParentBranch.Health / branch.ParentBranch.MaxHealth); + float parentHealth = branch.ParentBranch == null ? 0.0f : branch.ParentBranch.Health / branch.ParentBranch.MaxHealth; + float speed = MathHelper.Lerp(5.0f, 0.1f, parentHealth); DamageBranch(branch, speed * speed * deltaTime, AttackType.CutFromRoot); } if (branch.Health <= 0.0f) @@ -1071,6 +1081,25 @@ namespace Barotrauma.MapCreatures.Behavior } } + private void CheckDisconnectedFromRoot() + { + bool foundDisconnected; + do + { + foundDisconnected = false; + foreach (BallastFloraBranch branch in Branches) + { + if (branch.ParentBranch == null || branch.DisconnectedFromRoot) { continue; } + if (branch.ParentBranch.Removed || branch.ParentBranch.DisconnectedFromRoot) + { + branch.DisconnectedFromRoot = true; + foundDisconnected = true; + } + } + } while (foundDisconnected); + + } + public void RemoveBranch(BallastFloraBranch branch) { bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; @@ -1081,20 +1110,7 @@ namespace Barotrauma.MapCreatures.Behavior Branches.Remove(branch); branch.Removed = true; - bool foundDisconnected = false; - do - { - foundDisconnected = false; - foreach (BallastFloraBranch otherBranch in Branches) - { - if (otherBranch.ParentBranch == null || otherBranch.DisconnectedFromRoot) { continue; } - if (otherBranch.ParentBranch.Removed || otherBranch.ParentBranch.DisconnectedFromRoot) - { - otherBranch.DisconnectedFromRoot = true; - foundDisconnected = true; - } - } - } while (foundDisconnected); + CheckDisconnectedFromRoot(); bodies.ForEachMod(body => { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index d6be0f566..4dd4ad9f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -4056,7 +4056,11 @@ namespace Barotrauma string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value); BeaconStation = SpawnSubOnPath(beaconStationName, contentFile, SubmarineType.BeaconStation); - if (BeaconStation == null) { return; } + if (BeaconStation == null) + { + LevelData.HasBeaconStation = false; + return; + } Item sonarItem = Item.ItemList.Find(it => it.Submarine == BeaconStation && it.GetComponent() != null); if (sonarItem == null) @@ -4072,6 +4076,11 @@ namespace Barotrauma if (!LevelData.HasBeaconStation) { return; } if (GameMain.NetworkMember?.IsClient ?? false) { return; } + if (BeaconStation == null) + { + throw new InvalidOperationException("Failed to prepare beacon station (no beacon station in the level)."); + } + List beaconItems = Item.ItemList.FindAll(it => it.Submarine == BeaconStation); Item reactorItem = beaconItems.Find(it => it.GetComponent() != null); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index e3b3d3d81..42ca533d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -567,6 +567,10 @@ namespace Barotrauma /// public static void UpdateAll(float deltaTime, Camera cam) { +#if CLIENT + var sw = new System.Diagnostics.Stopwatch(); + sw.Start(); +#endif foreach (Hull hull in Hull.HullList) { hull.Update(deltaTime, cam); @@ -594,6 +598,11 @@ namespace Barotrauma gapUpdateTimer = 0; } +#if CLIENT + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Update:MapEntity:Misc", sw.ElapsedTicks); + sw.Restart(); +#endif Powered.UpdatePower(deltaTime); foreach (Item item in Item.ItemList) { @@ -602,6 +611,11 @@ namespace Barotrauma UpdateAllProjSpecific(deltaTime); +#if CLIENT + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Update:MapEntity:Items", sw.ElapsedTicks); + sw.Restart(); +#endif Spawner?.Update(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 18321db98..dc47f42f9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -1857,15 +1857,23 @@ namespace Barotrauma public void RefreshOutdoorNodes() => OutdoorNodes.ForEach(n => n?.Waypoint?.FindHull()); - public Item FindContainerFor(Item item, bool onlyPrimary, bool checkTransferConditions = false) + public Item FindContainerFor(Item item, bool onlyPrimary, bool checkTransferConditions = false, bool allowConnectedSubs = false) { - var potentialContainers = new List(); + var connectedSubs = GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player).ToHashSet(); + Item selectedContainer = null; foreach (Item potentialContainer in Item.ItemList) { if (potentialContainer.Removed) { continue; } if (potentialContainer.NonInteractable) { continue; } if (potentialContainer.HiddenInGame) { continue; } - if (potentialContainer.Submarine != this) { continue; } + if (allowConnectedSubs) + { + if (!connectedSubs.Contains(potentialContainer.Submarine)) { continue; } + } + else + { + if (potentialContainer.Submarine != this) { continue; } + } if (potentialContainer == item) { continue; } if (potentialContainer.Condition <= 0) { continue; } if (potentialContainer.OwnInventory == null) { continue; } @@ -1875,13 +1883,15 @@ namespace Barotrauma if (!potentialContainer.OwnInventory.CanBePut(item)) { continue; } if (!container.ShouldBeContained(item, out _)) { continue; } if (!item.Prefab.IsContainerPreferred(item, container, out bool isPreferencesDefined, out bool isSecondary, checkTransferConditions: checkTransferConditions) || !isPreferencesDefined || onlyPrimary && isSecondary) { continue; } - potentialContainers.Add(potentialContainer); - if (!isSecondary) + if (potentialContainer.Submarine == this && !isSecondary) { - break; + //valid primary container in the same sub -> perfect, let's use that one + return potentialContainer; } + selectedContainer = potentialContainer; + } - return potentialContainers.LastOrDefault(); + return selectedContainer; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs index 3706116ef..64e8c5517 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs @@ -117,6 +117,8 @@ namespace Barotrauma.Networking set; } + public ChatMode ChatMode { get; set; } = ChatMode.None; + protected ChatMessage(string senderName, string text, ChatMessageType type, Character sender, Client client, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None, Color? textColor = null) { Text = text; diff --git a/Barotrauma/BarotraumaShared/SharedSource/PerformanceCounter.cs b/Barotrauma/BarotraumaShared/SharedSource/PerformanceCounter.cs index bf0a96dd4..f72637960 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/PerformanceCounter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/PerformanceCounter.cs @@ -23,7 +23,6 @@ namespace Barotrauma private readonly Dictionary> elapsedTicks = new Dictionary>(); private readonly Dictionary avgTicksPerFrame = new Dictionary(); - private readonly Dictionary> partialTickInfos = new Dictionary>(); #if CLIENT internal Graph UpdateTimeGraph = new Graph(500), DrawTimeGraph = new Graph(500); @@ -43,20 +42,6 @@ namespace Barotrauma } } - private readonly List tempSavedPartialIdentifiers = new List(); - public IReadOnlyList GetSavedPartialIdentifiers(string parentIdentifier) - { - lock (mutex) - { - tempSavedPartialIdentifiers.Clear(); - if (partialTickInfos.TryGetValue(parentIdentifier, out var tickInfos)) - { - tempSavedPartialIdentifiers.AddRange(tickInfos.Keys); - } - } - return tempSavedPartialIdentifiers; - } - public void AddElapsedTicks(string identifier, long ticks) { lock (mutex) @@ -72,29 +57,6 @@ namespace Barotrauma } } - public void AddPartialElapsedTicks(string parentIdentifier, string identifier, long ticks) - { - lock (mutex) - { - if (!partialTickInfos.TryGetValue(parentIdentifier, out var tickInfos)) - { - tickInfos = new Dictionary(); - partialTickInfos.Add(parentIdentifier, tickInfos); - } - if (!tickInfos.TryGetValue(identifier, out var tickInfo)) - { - tickInfo = new TickInfo(); - tickInfos.Add(identifier, tickInfo); - } - tickInfo.ElapsedTicks.Enqueue(ticks); - if (tickInfo.ElapsedTicks.Count > MaximumSamples) - { - tickInfo.ElapsedTicks.Dequeue(); - tickInfo.AvgTicksPerFrame = (long)tickInfo.ElapsedTicks.Average(i => i); - } - } - } - public float GetAverageElapsedMillisecs(string identifier) { long ticksPerFrame = 0; @@ -105,18 +67,6 @@ namespace Barotrauma return ticksPerFrame * 1000.0f / Stopwatch.Frequency; } - public float GetPartialAverageElapsedMillisecs(string parentIdentifier, string identifier) - { - long ticksPerFrame = 0; - lock (mutex) - { - if (!partialTickInfos.TryGetValue(parentIdentifier, out var tickInfos)) { return 0.0f; } - if (!tickInfos.TryGetValue(identifier, out var tickInfo)) { return 0.0f; } - ticksPerFrame = tickInfo.AvgTicksPerFrame; - } - return ticksPerFrame * 1000.0f / Stopwatch.Frequency; - } - public bool Update(double deltaTime) { if (deltaTime == 0.0f) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index e7c0cecab..eaedf8556 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -149,19 +149,19 @@ namespace Barotrauma #if CLIENT sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("GameSessionUpdate", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:GameSession", sw.ElapsedTicks); sw.Restart(); GameMain.ParticleManager.Update((float)deltaTime); sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("ParticleUpdate", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:Particles", sw.ElapsedTicks); sw.Restart(); if (Level.Loaded != null) Level.Loaded.Update((float)deltaTime, cam); sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("LevelUpdate", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:Level", sw.ElapsedTicks); if (Character.Controlled != null) { @@ -193,7 +193,7 @@ namespace Barotrauma #if CLIENT sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("CharacterUpdate", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:Character", sw.ElapsedTicks); sw.Restart(); #endif @@ -201,7 +201,7 @@ namespace Barotrauma #if CLIENT sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("StatusEffectUpdate", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:StatusEffects", sw.ElapsedTicks); sw.Restart(); if (Character.Controlled != null && @@ -253,7 +253,7 @@ namespace Barotrauma #if CLIENT sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("MapEntityUpdate", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:MapEntity", sw.ElapsedTicks); sw.Restart(); #endif Character.UpdateAnimAll((float)deltaTime); @@ -266,7 +266,7 @@ namespace Barotrauma #if CLIENT sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("AnimUpdate", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:Ragdolls", sw.ElapsedTicks); sw.Restart(); #endif @@ -277,7 +277,7 @@ namespace Barotrauma #if CLIENT sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("SubmarineUpdate", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:Submarine", sw.ElapsedTicks); sw.Restart(); #endif @@ -297,7 +297,7 @@ namespace Barotrauma #if CLIENT sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("Physics", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:Physics", sw.ElapsedTicks); #endif UpdateProjSpecific(deltaTime); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 8d73ee087..097f1c888 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -1119,7 +1119,12 @@ namespace Barotrauma itemComponent.SetRequiredItems(element, allowEmpty: true); break; } - } + } + if (itemComponent is ItemContainer itemContainer && + (componentElement.GetChildElement("containable") != null || componentElement.GetChildElement("subcontainer") != null)) + { + itemContainer.LoadContainableRestrictions(componentElement); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index 600b2161d..f510d30da 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -226,6 +226,7 @@ namespace Barotrauma { MusicVolume = 0.3f, SoundVolume = 0.5f, + UiVolume = 0.3f, VoiceChatVolume = 0.5f, VoiceChatCutoffPrevention = 0, MicrophoneVolume = 5, @@ -234,7 +235,6 @@ namespace Barotrauma UseDirectionalVoiceChat = true, VoipAttenuationEnabled = true, VoiceSetting = VoiceMode.PushToTalk, - UseLocalVoiceByDefault = false, DisableVoiceChatFilters = false }; return audioSettings; @@ -249,6 +249,7 @@ namespace Barotrauma public float MusicVolume; public float SoundVolume; + public float UiVolume; public float VoiceChatVolume; public int VoiceChatCutoffPrevention; public float MicrophoneVolume; @@ -264,7 +265,6 @@ namespace Barotrauma public string VoiceCaptureDevice; public float NoiseGateThreshold; - public bool UseLocalVoiceByDefault; public bool DisableVoiceChatFilters; } @@ -286,12 +286,13 @@ namespace Barotrauma { InputType.Aim, MouseButton.SecondaryMouse }, { InputType.InfoTab, Keys.Tab }, - { InputType.Chat, Keys.T }, - { InputType.RadioChat, Keys.R }, + { InputType.Chat, Keys.None }, + { InputType.RadioChat, Keys.None }, + { InputType.ActiveChat, Keys.T }, { InputType.CrewOrders, Keys.C }, { InputType.Voice, Keys.V }, - { InputType.LocalVoice, Keys.B }, + { InputType.ToggleChatMode, Keys.R }, { InputType.Command, MouseButton.MiddleMouse }, { InputType.PreviousFireMode, MouseButton.MouseWheelDown }, { InputType.NextFireMode, MouseButton.MouseWheelUp }, @@ -332,17 +333,35 @@ namespace Barotrauma if (!bindings.ContainsKey(inputType)) { bindings.Add(inputType, defaultBindings[inputType]); } } + bool playerConfigContainsNewChatBinds = false; foreach (XElement element in elements) { foreach (XAttribute attribute in element.Attributes()) { if (Enum.TryParse(attribute.Name.LocalName, out InputType result)) { + if (!playerConfigContainsNewChatBinds) + { + playerConfigContainsNewChatBinds = result == InputType.ActiveChat; + } bindings[result] = element.GetAttributeKeyOrMouse(attribute.Name.LocalName, bindings[result]); } } } + // Clear the old chat binds for configs saved before the introduction of the new chat binds + if (!playerConfigContainsNewChatBinds) + { + if (bindings.ContainsKey(InputType.Chat)) + { + bindings[InputType.Chat] = Keys.None; + } + if (bindings.ContainsKey(InputType.RadioChat)) + { + bindings[InputType.RadioChat] = Keys.None; + } + } + Bindings = bindings.ToImmutableDictionary(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index eebf72549..3fae997c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -157,7 +157,7 @@ namespace Barotrauma /// Should the item spawn even if the container can't contain items of this type /// public readonly bool SpawnIfCantBeContained; - public readonly float Speed; + public readonly float Impulse; public readonly float Rotation; public readonly int Count; public readonly float Spread; @@ -198,7 +198,7 @@ namespace Barotrauma SpawnIfInventoryFull = element.GetAttributeBool("spawnifinventoryfull", false); SpawnIfCantBeContained = element.GetAttributeBool("spawnifcantbecontained", true); - Speed = element.GetAttributeFloat("speed", 0.0f); + Impulse = element.GetAttributeFloat("impulse", element.GetAttributeFloat("speed", 0.0f)); Condition = MathHelper.Clamp(element.GetAttributeFloat("condition", 1.0f), 0.0f, 1.0f); @@ -1708,7 +1708,7 @@ namespace Barotrauma throw new NotImplementedException("Spawn rotation type not implemented: " + chosenItemSpawnInfo.RotationType); } body.SetTransform(newItem.SimPosition, rotation); - body.ApplyLinearImpulse(Rand.Vector(1) * chosenItemSpawnInfo.Speed); + body.ApplyLinearImpulse(Rand.Vector(1) * chosenItemSpawnInfo.Impulse); } } newItem.Condition = newItem.MaxCondition * chosenItemSpawnInfo.Condition; diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 1cd44dc4e..3c31ab7de 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,38 @@ +--------------------------------------------------------------------------------------------------------- +v0.18.5.0 +--------------------------------------------------------------------------------------------------------- + +Chat improvements: +- Chat mode (radio/local) can be toggled using a dropdown next to the chat box or with a dedicated "ToggleChatMode" keybind (R by default). +- Voice chat now has only one push-to-talk keybind (V by default) which respects the selected chat mode. +- There's now a dedicated "ActiveChat" keybind (T by default) to open the chat using the currently active chat mode. +- If you want to keep the chat keybinds the way they were (separate keybinds for local and radio), you can rebind the "Chat" and "RadioChat" inputs back to T and R and the new "ToggleChatMode" and "ActiveChat" inputs to something else. + +Changes: +- Optimized bot AIs: in particular, the cleanup, repair, pump water and load items objectives. Should significantly improve performance when the bots are doing these objectives when there's a large number of items in the sub. +- Optimized entity culling logic (determines which items/structures are currently visible in the screen). +- Optimized a bunch of textures. +- Improved the performance statistics view that's enabled with the "showperf" console command: more fine-grained stats and easier-to-read visuals. +- Added UI volume slider. +- Depth charges can be stored in coilgun ammo shelves. + +Fixes: +- Fixed ballast flora branches that have been disconnected from the root not being considered disconnected after a level transition (allowing them to keep growing). +- Fixed "set default bindings" not doing anything in the settings menu. +- Fixed door/hatch gaps not getting moved when snapping to grid in the sub editor. +- Vertically mirrored beds can't be laid on. +- Fixed wrecked reactors being forced to non-interactable even if made interactable in the sub editor. + +Unstable only: +- Fixed crashing when you try to edit beacon station settings on a station you haven't saved yet. +- Fixed level difficulties being incorrect in the "normal" difficulty setting (levels were set to the minimum difficulty of the biome, instead of a linear increase across the biome). +- Misc fixes and improvements to the new beacon stations. +- Fixed "max missions per round" value not being restricted in the UI. +- Fixed items not getting transferred to/from linked subs. + +Modding: +- Doors and hatches can now be mirrored in the sub editor (making them open from top to bottom, or from right to left). + --------------------------------------------------------------------------------------------------------- v0.18.4.0 --------------------------------------------------------------------------------------------------------- @@ -12,6 +47,7 @@ Unstable only: - Fixed sprite editor crashing if you try to reload a texture twice. - Fixed drone/shuttles getting left behind in the outpost when you buy and switch to a sub that has a one. - Fixed crashing when closing the submarine preview window. +- Disallowed mirroring beds vertically. Changes: - Added some new campaign settings: starting balance, amount of starting items and difficulty. @@ -19,13 +55,17 @@ Changes: - Made the descriptions of some materials (that used to just say "useful for crafting") more descriptive. - Increased oxygen generator output in some vanilla subs. - Made handheld sonar beacon sound less grating. -- Disallowed mirroring beds vertically. - The client who initiated a vote cannot take part in that vote (except if they're the only client who can vote, in which case the vote automatically passes). - Made flashlight flicker before the battery runs out. - Added some lootable money to corpses found in wrecks. - Removed the small equipment indicators next to the character portrait. - Added a bunch of new UI sounds (tickbox toggling, confirming transactions, increase/decrease sounds for number inputs, cart sound for adding/removing items in store interfaces, selecting/clicking components, sliders and modlist). - +- Optimized many status effects by making them only execute once per second instead of every frame (most importantly, diving suits and volatile fuel rods). +- Optimized talents: buffs are applied to characters periodically instead of every frame. +- Optimized the logic that bots use to determine the safety of hulls. +- Optimized items: stop updating items that don't need to be updated more aggressively. +- Weapon holders now use the tag "mountableweapon" instead of "weapon" to determine which items can be placed in them. Allows tagging non-weapon items as mountable in the holder, without making bots consider it a weapon due to the "weapon" tag. Also allows to keep some weapons not-mountable. +- Ammunition Shelf can now also store Depth Charges ("depthchargeammo" tag added) Fixes: - Split campaign state networking messages into multiple ones. Previously all the campaign-related data (map state, reputation, upgrades, purchased items, selected missions) was included in the same message, and whenever anything in the data changed, the server would send all of it to clients. This would cause performance and bandwidth issues in some situations, for example when reputation was changing rapidly. From 5a10b444ee027eb251e6169047e0705f2b4d0a83 Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Sat, 4 Jun 2022 00:34:02 +0900 Subject: [PATCH 06/14] Build 0.18.6.0 --- Barotrauma/BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- Barotrauma/BarotraumaClient/WindowsClient.csproj | 2 +- Barotrauma/BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- Barotrauma/BarotraumaServer/WindowsServer.csproj | 2 +- .../BarotraumaShared/SharedSource/Items/Components/Door.cs | 5 ----- Barotrauma/BarotraumaShared/changelog.txt | 7 +++++++ 8 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index c0771a588..812630849 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.5.0 + 0.18.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 46e1ecfdf..3907f2088 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.5.0 + 0.18.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 3fdf81956..138834ca7 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.5.0 + 0.18.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 865d79238..6abd0ba2e 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.5.0 + 0.18.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 0e97bc036..fa7a15b47 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.5.0 + 0.18.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index f9f448b02..f8b909704 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.5.0 + 0.18.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index eca3653c0..aa26c2e1d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -270,11 +270,6 @@ namespace Barotrauma.Items.Components { Body?.SetTransform(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f); } - if (linkedGap != null) - { - RefreshLinkedGap(); - linkedGap.Rect = item.Rect; - } #if CLIENT UpdateConvexHulls(); #endif diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 3c31ab7de..3d5d91ced 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,10 @@ +--------------------------------------------------------------------------------------------------------- +v0.18.6.0 +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Fixed crashing during outpost generation. + --------------------------------------------------------------------------------------------------------- v0.18.5.0 --------------------------------------------------------------------------------------------------------- From 564656eded8b45f46dce9d69d5745b513d36da99 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Tue, 7 Jun 2022 19:54:32 +0300 Subject: [PATCH 07/14] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index ee2a81205..216c02877 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -53,8 +53,8 @@ body: description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu. options: - 0.17.15.0 - - 0.18.3.0 (Unstable) - - 0.18.2.0 (Unstable) + - 0.18.6.0 (Unstable) + - 0.18.5.0 (Unstable) - Other validations: required: true From 4f5a3bf8b9839dc292a0448954d3e650612f4265 Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Wed, 8 Jun 2022 21:52:38 +0900 Subject: [PATCH 08/14] Build 0.18.7.0 --- .../ClientSource/Characters/CharacterHUD.cs | 1 - .../ClientSource/GUI/ChatBox.cs | 5 ++ .../ClientSource/GUI/HUDLayoutSettings.cs | 4 +- .../ClientSource/GUI/TabMenu.cs | 8 ++-- .../GameModes/Tutorials/CaptainTutorial.cs | 26 ++--------- .../GameModes/Tutorials/DoctorTutorial.cs | 21 +++------ .../GameModes/Tutorials/EngineerTutorial.cs | 9 ++-- .../GameModes/Tutorials/MechanicTutorial.cs | 20 ++++---- .../GameModes/Tutorials/OfficerTutorial.cs | 36 +++------------ .../ClientSource/GameSession/ReadyCheck.cs | 20 ++++---- .../ClientSource/Items/Components/Door.cs | 3 +- .../Items/Components/ItemComponent.cs | 2 +- .../Items/Components/Signal/Terminal.cs | 2 + .../BarotraumaClient/ClientSource/Map/Gap.cs | 2 +- .../ClientSource/Map/SubmarineInfo.cs | 2 +- .../ClientSource/Networking/GameClient.cs | 7 +++ .../Networking/OrderChatMessage.cs | 1 + .../Networking/Primitives/Peers/ClientPeer.cs | 2 +- .../Primitives/Peers/LidgrenClientPeer.cs | 2 - .../CampaignSetupUI/CampaignSetupUI.cs | 2 +- .../SinglePlayerCampaignSetupUI.cs | 15 ++++-- .../ClientSource/Screens/LevelEditorScreen.cs | 12 ++--- .../ClientSource/Screens/SubEditorScreen.cs | 35 +++++++------- .../ClientSource/Settings/SettingsMenu.cs | 4 +- .../ClientSource/Sprite/Sprite.cs | 4 +- .../WorkshopMenu/Mutable/InstalledTab.cs | 8 ++-- .../Steam/WorkshopMenu/Mutable/ItemList.cs | 6 +++ .../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 | 5 ++ .../GameModes/MultiPlayerCampaign.cs | 4 +- .../ServerSource/GameSession/ReadyCheck.cs | 11 +++-- .../Items/Components/Machines/Pump.cs | 17 +++++-- .../BarotraumaServer/ServerSource/Map/Hull.cs | 28 +++++------ .../ServerSource/Networking/Client.cs | 3 ++ .../ServerSource/Networking/GameServer.cs | 18 ++++++-- .../ServerSource/Networking/RespawnManager.cs | 8 ++++ .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Characters/Animation/Ragdoll.cs | 8 +++- .../Events/EventActions/SpawnAction.cs | 5 +- .../SharedSource/GameSession/ReadyCheck.cs | 29 +++++++----- .../Items/Components/Machines/Pump.cs | 10 ++-- .../Items/Components/Power/Powered.cs | 8 +++- .../Items/Components/Signal/Connection.cs | 1 + .../Map/Creatures/BallastFloraBehavior.cs | 32 +++++++++---- .../SharedSource/Map/LinkedSubmarine.cs | 30 ++++++++---- .../Map/Outposts/OutpostGenerationParams.cs | 43 +++++++++++++---- .../Map/Outposts/OutpostGenerator.cs | 46 ++++++++++--------- .../SharedSource/Map/SubmarineInfo.cs | 4 +- .../Serialization/SerializableProperty.cs | 5 ++ .../SharedSource/Settings/GameSettings.cs | 6 +++ .../SharedSource/Utils/ToolBox.cs | 16 +++++-- Barotrauma/BarotraumaShared/changelog.txt | 38 +++++++++++++++ 56 files changed, 401 insertions(+), 245 deletions(-) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index b6304d486..02e09c587 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -3,7 +3,6 @@ using Barotrauma.Items.Components; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; using System.Linq; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index a6a44048a..114d62cdc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -287,6 +287,11 @@ namespace Barotrauma InputBox.OnDeselected += (gui, Keys) => { ChatManager.Clear(); + if (GUIFrame.IsParentOf(GUI.MouseOn)) + { + CloseAfterMessageSent = false; + return; + } ChatMessage.GetChatMessageCommand(InputBox.Text, out var message); if (string.IsNullOrEmpty(message)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index 7afce155d..1c2e869b0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -15,7 +15,7 @@ namespace Barotrauma get { return inventoryTopY; } set { - if (value == inventoryTopY) return; + if (value == inventoryTopY) { return; } inventoryTopY = value; CreateAreas(); } @@ -91,8 +91,6 @@ namespace Barotrauma if (GameMain.Instance != null) { GameMain.Instance.ResolutionChanged += CreateAreas; - #warning TODO: reimplement - //GameSettings.CurrentConfig.OnHUDScaleChanged += CreateAreas; CreateAreas(); CharacterInfo.Init(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 47752417d..ecdb03592 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -1559,10 +1559,10 @@ namespace Barotrauma RichString missionReputationString = RichString.Rich(reputationText, wrapMissionText(GUIStyle.Font)); RichString missionDescriptionString = RichString.Rich(descriptionText, wrapMissionText(GUIStyle.Font)); - Vector2 missionNameSize = GUIStyle.LargeFont.MeasureString(missionNameString); - Vector2 missionDescriptionSize = GUIStyle.Font.MeasureString(missionDescriptionString); - Vector2 missionRewardSize = GUIStyle.Font.MeasureString(missionRewardString); - Vector2 missionReputationSize = GUIStyle.Font.MeasureString(missionReputationString); + Vector2 missionNameSize = GUIStyle.LargeFont.MeasureString(missionNameString.SanitizedValue); + Vector2 missionDescriptionSize = GUIStyle.Font.MeasureString(missionDescriptionString.SanitizedValue); + Vector2 missionRewardSize = GUIStyle.Font.MeasureString(missionRewardString.SanitizedValue); + Vector2 missionReputationSize = GUIStyle.Font.MeasureString(missionReputationString.SanitizedValue); float ySize = missionNameSize.Y + missionDescriptionSize.Y + missionRewardSize.Y + missionReputationSize.Y + missionTextGroup.AbsoluteSpacing * 4; bool displayDifficulty = mission.Difficulty.HasValue; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs index 2f9e0a46f..c4ec80301 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs @@ -102,29 +102,11 @@ namespace Barotrauma.Tutorials radioSpeakerName = TextManager.Get("Tutorial.Radio.Watchman"); GameMain.GameSession.CrewManager.AllowCharacterSwitch = false; - var revolver = FindOrGiveItem(captain, "revolver".ToIdentifier()); - revolver.Unequip(captain); - captain.Inventory.RemoveItem(revolver); - - var captainscap = - captain.Inventory.FindItemByIdentifier("captainscap1".ToIdentifier()) ?? - captain.Inventory.FindItemByIdentifier("captainscap2".ToIdentifier()) ?? - captain.Inventory.FindItemByIdentifier("captainscap3".ToIdentifier()); - - if (captainscap != null) + foreach (Item item in captain.Inventory.AllItemsMod) { - captainscap.Unequip(captain); - captain.Inventory.RemoveItem(captainscap); - } - - var captainsuniform = - captain.Inventory.FindItemByIdentifier("captainsuniform1".ToIdentifier()) ?? - captain.Inventory.FindItemByIdentifier("captainsuniform2".ToIdentifier()) ?? - captain.Inventory.FindItemByIdentifier("captainsuniform3".ToIdentifier()); - if (captainsuniform != null) - { - captainsuniform.Unequip(captain); - captain.Inventory.RemoveItem(captainsuniform); + if (item.HasTag("identitycard") || item.HasTag("headset")) { continue; } + item.Unequip(captain); + captain.Inventory.RemoveItem(item); } var steerOrder = OrderPrefab.Prefabs["steer"]; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs index 4cf46e5f1..ff73b2f00 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs @@ -105,21 +105,12 @@ namespace Barotrauma.Tutorials radioSpeakerName = TextManager.Get("Tutorial.Radio.Speaker"); doctor = Character.Controlled; - var bandages = FindOrGiveItem(doctor, "antibleeding1".ToIdentifier()); - bandages.Unequip(doctor); - doctor.Inventory.RemoveItem(bandages); - - var syringegun = FindOrGiveItem(doctor, "syringegun".ToIdentifier()); - syringegun.Unequip(doctor); - doctor.Inventory.RemoveItem(syringegun); - - var antibiotics = FindOrGiveItem(doctor, "antibiotics".ToIdentifier()); - antibiotics.Unequip(doctor); - doctor.Inventory.RemoveItem(antibiotics); - - var morphine = FindOrGiveItem(doctor, "antidama1".ToIdentifier()); - morphine.Unequip(doctor); - doctor.Inventory.RemoveItem(morphine); + foreach (Item item in doctor.Inventory.AllItemsMod) + { + if (item.HasTag("clothing") || item.HasTag("identitycard") || item.HasTag("headset")) { continue; } + item.Unequip(doctor); + doctor.Inventory.RemoveItem(item); + } doctor_suppliesCabinet = Item.ItemList.Find(i => i.HasTag("doctor_suppliescabinet"))?.GetComponent(); doctor_medBayCabinet = Item.ItemList.Find(i => i.HasTag("doctor_medbaycabinet"))?.GetComponent(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs index 52592248d..c30c1c346 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs @@ -131,9 +131,12 @@ namespace Barotrauma.Tutorials radioSpeakerName = TextManager.Get("Tutorial.Radio.Speaker"); engineer = Character.Controlled; - var toolbelt = FindOrGiveItem(engineer, "toolbelt".ToIdentifier()); - toolbelt.Unequip(engineer); - engineer.Inventory.RemoveItem(toolbelt); + foreach (Item item in engineer.Inventory.AllItemsMod) + { + if (item.HasTag("clothing") || item.HasTag("identitycard") || item.HasTag("headset")) { continue; } + item.Unequip(engineer); + engineer.Inventory.RemoveItem(item); + } var repairOrder = OrderPrefab.Prefabs["repairsystems"]; engineer_repairIcon = repairOrder.SymbolSprite; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs index 71fd7e344..23b5da096 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs @@ -160,13 +160,12 @@ namespace Barotrauma.Tutorials radioSpeakerName = TextManager.Get("Tutorial.Radio.Speaker"); mechanic = Character.Controlled; - var toolbelt = FindOrGiveItem(mechanic, "toolbelt".ToIdentifier()); - toolbelt.Unequip(mechanic); - mechanic.Inventory.RemoveItem(toolbelt); - - var crowbar = FindOrGiveItem(mechanic, "crowbar".ToIdentifier()); - crowbar.Unequip(mechanic); - mechanic.Inventory.RemoveItem(crowbar); + foreach (Item item in mechanic.Inventory.AllItemsMod) + { + if (item.HasTag("clothing") || item.HasTag("identitycard") || item.HasTag("headset")) { continue; } + item.Unequip(mechanic); + mechanic.Inventory.RemoveItem(item); + } var repairOrder = OrderPrefab.Prefabs["repairsystems"]; mechanic_repairIcon = repairOrder.SymbolSprite; @@ -297,7 +296,10 @@ namespace Barotrauma.Tutorials public override void Update(float deltaTime) { - mechanic_brokenhull_1.WaterVolume = MathHelper.Clamp(mechanic_brokenhull_1.WaterVolume, 0, mechanic_brokenhull_1.Volume * 0.85f); + if (mechanic_brokenhull_1 != null) + { + mechanic_brokenhull_1.WaterVolume = MathHelper.Clamp(mechanic_brokenhull_1.WaterVolume, 0, mechanic_brokenhull_1.Volume * 0.85f); + } base.Update(deltaTime); } @@ -413,7 +415,7 @@ namespace Barotrauma.Tutorials } } while (mechanic_workingPump.FlowPercentage >= 0 || !mechanic_workingPump.IsActive); // Highlight until draining SetHighlight(mechanic_workingPump.Item, false); - do { yield return null; } while (mechanic_brokenhull_1.WaterPercentage > waterVolumeBeforeOpening); // Unlock door once drained + do { yield return null; } while (mechanic_brokenhull_1 != null && mechanic_brokenhull_1.WaterPercentage > waterVolumeBeforeOpening); // Unlock door once drained RemoveCompletedObjective(3); GameAnalyticsManager.AddDesignEvent("Tutorial:MechanicTutorial:Objective3"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs index b2514450f..a8ba18da8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs @@ -141,36 +141,12 @@ namespace Barotrauma.Tutorials radioSpeakerName = TextManager.Get("Tutorial.Radio.Speaker"); officer = Character.Controlled; - var handcuffs = FindOrGiveItem(officer, "handcuffs".ToIdentifier()); - handcuffs.Unequip(officer); - officer.Inventory.RemoveItem(handcuffs); - - var stunbaton = FindOrGiveItem(officer, "stunbaton".ToIdentifier()); - stunbaton.Unequip(officer); - officer.Inventory.RemoveItem(stunbaton); - - var smg = FindOrGiveItem(officer, "smg".ToIdentifier()); - smg.Unequip(officer); - officer.Inventory.RemoveItem(smg); - - var divingknife = FindOrGiveItem(officer, "divingknife".ToIdentifier()); - divingknife.Unequip(officer); - officer.Inventory.RemoveItem(divingknife); - - var steroids = FindOrGiveItem(officer, "steroids".ToIdentifier()); - steroids.Unequip(officer); - officer.Inventory.RemoveItem(steroids); - - var ballistichelmet = - officer.Inventory.FindItemByIdentifier("ballistichelmet1".ToIdentifier()) ?? - officer.Inventory.FindItemByIdentifier("ballistichelmet2".ToIdentifier()) ?? - FindOrGiveItem(officer, "ballistichelmet3".ToIdentifier()); - ballistichelmet.Unequip(officer); - officer.Inventory.RemoveItem(ballistichelmet); - - var bodyarmor = FindOrGiveItem(officer, "bodyarmor".ToIdentifier()); - bodyarmor.Unequip(officer); - officer.Inventory.RemoveItem(bodyarmor); + foreach (Item item in officer.Inventory.AllItemsMod) + { + if (item.HasTag("clothing") || item.HasTag("identitycard") || item.HasTag("headset")) { continue; } + item.Unequip(officer); + officer.Inventory.RemoveItem(item); + } var gunOrder = OrderPrefab.Prefabs["operateweapons"]; officer_gunIcon = gunOrder.SymbolSprite; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs index 003816dae..4db03f290 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs @@ -28,7 +28,7 @@ namespace Barotrauma UserListData = "ReadyUserList", ReadySpriteData = "ReadySprite"; - private int lastSecond; + private int lastSecond = 1; private GUIMessageBox? msgBox; private GUIMessageBox? resultsBox; @@ -44,7 +44,7 @@ namespace Barotrauma msgBox = new GUIMessageBox(readyCheckHeader, readyCheckBody(author), new[] { yesButton, noButton }, relativeSize, minSize, type: GUIMessageBox.Type.Vote) { UserData = PromptData, Draggable = true }; GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.125f), msgBox.Content.RectTransform), childAnchor: Anchor.Center); - new GUIProgressBar(new RectTransform(new Vector2(0.8f, 1f), contentLayout.RectTransform), time / endTime, GUIStyle.Orange) { UserData = TimerData }; + new GUIProgressBar(new RectTransform(new Vector2(0.8f, 1f), contentLayout.RectTransform), 0.0f, GUIStyle.Orange) { UserData = TimerData }; // Yes msgBox.Buttons[0].OnClicked = delegate @@ -116,17 +116,18 @@ namespace Barotrauma private void UpdateBar() { + double elapsedTime = (DateTime.Now - startTime).TotalSeconds; if (msgBox != null && !msgBox.Closed && GUIMessageBox.MessageBoxes.Contains(msgBox)) { if (msgBox.FindChild(TimerData, true) is GUIProgressBar bar) { - bar.BarSize = time / endTime; + bar.BarSize = (float)(elapsedTime / (endTime - startTime).TotalSeconds); } } // play click sound after a second has passed - int second = (int) Math.Ceiling(time); - if (second < lastSecond) + int second = (int)Math.Ceiling(elapsedTime); + if (second > lastSecond) { if (msgBox != null && !msgBox.Closed) { @@ -156,7 +157,8 @@ namespace Barotrauma bool isOwn = false; byte authorId = 0; - float duration = inc.ReadSingle(); + long startTime = inc.ReadInt64(); + long endTime = inc.ReadInt64(); string author = inc.ReadString(); bool hasAuthor = inc.ReadBoolean(); @@ -173,7 +175,9 @@ namespace Barotrauma clients.Add(inc.ReadByte()); } - ReadyCheck rCheck = new ReadyCheck(clients, duration); + ReadyCheck rCheck = new ReadyCheck(clients, + DateTimeOffset.FromUnixTimeSeconds(startTime).LocalDateTime, + DateTimeOffset.FromUnixTimeSeconds(endTime).LocalDateTime); crewManager.ActiveReadyCheck = rCheck; if (isOwn) @@ -192,12 +196,10 @@ namespace Barotrauma } break; case ReadyCheckState.Update: - float time = inc.ReadSingle(); ReadyStatus newState = (ReadyStatus) inc.ReadByte(); byte targetId = inc.ReadByte(); if (crewManager.ActiveReadyCheck != null) { - crewManager.ActiveReadyCheck.time = time; crewManager.ActiveReadyCheck?.UpdateState(targetId, newState); } break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index 3d3070aef..88bd89496 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -222,7 +222,8 @@ namespace Barotrauma.Items.Components if (brokenSprite != null && item.Health < item.MaxCondition) { - Vector2 scale = scaleBrokenSprite ? new Vector2(1.0f, 1.0f - item.Health / item.MaxCondition) : Vector2.One; + Vector2 scale = scaleBrokenSprite ? new Vector2(1.0f - item.Health / item.MaxCondition) : Vector2.One; + if (IsHorizontal) { scale.X = 1; } else { scale.Y = 1; } float alpha = fadeBrokenSprite ? 1.0f - item.Health / item.MaxCondition : 1.0f; spriteBatch.Draw(brokenSprite.Texture, pos, getSourceRect(brokenSprite, openState, IsHorizontal), diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 4966e5c49..9b287cbd1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -405,7 +405,7 @@ namespace Barotrauma.Items.Components float newVolume; try { - newVolume = property.GetFloatValue(this); + newVolume = Math.Min(property.GetFloatValue(this), 1.0f); } catch { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index 8b97d27be..980a6f256 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -58,6 +58,8 @@ namespace Barotrauma.Items.Components return true; } }; + + layoutGroup.Recalculate(); } // Create fillerBlock to cover historyBox so new values appear at the bottom of historyBox diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs index 57979fdd0..088ffdf3d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs @@ -33,7 +33,7 @@ namespace Barotrauma DrawArrow(FlowTargetHull, IsHorizontal ? rect.Height: rect.Width, Math.Abs(lerpedFlowForce.Length()), Color.Red * 0.3f); } - if (outsideCollisionBlocker.Enabled && Submarine != null) + if (Submarine != null && outsideCollisionBlocker != null && outsideCollisionBlocker.Enabled) { var edgeShape = outsideCollisionBlocker.FixtureList[0].Shape as FarseerPhysics.Collision.Shapes.EdgeShape; Vector2 startPos = ConvertUnits.ToDisplayUnits(outsideCollisionBlocker.GetWorldPoint(edgeShape.Vertex1)) + Submarine.Position; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs index d0a908933..463d94253 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs @@ -8,7 +8,7 @@ namespace Barotrauma partial class SubmarineInfo : IDisposable { public Sprite PreviewImage; - + partial void InitProjectSpecific() { string previewImageData = SubmarineElement.GetAttributeString("previewimage", ""); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 4409512fb..2419901fd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -1200,7 +1200,14 @@ namespace Barotrauma.Networking new LocalizedString[] { TextManager.Get("Cancel") }); reconnectBox.Buttons[0].OnClicked += (btn, userdata) => { CancelConnect(); return true; }; connected = false; + + var prevContentPackages = clientPeer.ServerContentPackages; ConnectToServer(serverEndpoint, serverName); + if (clientPeer != null) + { + //restore the previous list of content packages so we can reconnect immediately without having to recheck that the packages match + clientPeer.ServerContentPackages = prevContentPackages; + } } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs index 1134f3aa0..673a8423f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs @@ -9,6 +9,7 @@ namespace Barotrauma.Networking msg.Write((byte)ClientNetObject.CHAT_MESSAGE); msg.Write(NetStateID); msg.WriteRangedInteger((int)ChatMessageType.Order, 0, Enum.GetValues(typeof(ChatMessageType)).Length - 1); + msg.WriteRangedInteger((int)ChatMode.None, 0, Enum.GetValues(typeof(ChatMode)).Length - 1); WriteOrder(msg); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index 3134ee4f8..f39baaaec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -50,7 +50,7 @@ namespace Barotrauma.Networking } } - public ImmutableArray ServerContentPackages { get; private set; } = + public ImmutableArray ServerContentPackages { get; set; } = ImmutableArray.Empty; public delegate void MessageCallback(IReadMessage message); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index 47201cef6..ed3738964 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -117,8 +117,6 @@ namespace Barotrauma.Networking PacketHeader packetHeader = (PacketHeader)inc.ReadByte(); - //Console.WriteLine(isCompressed + " " + isConnectionInitializationStep + " " + (int)incByte); - if (packetHeader.IsConnectionInitializationStep() && initializationStep != ConnectionInitialization.Success) { ReadConnectionInitializationStep(new ReadWriteMessage(inc.Data, (int)inc.Position, inc.LengthBits, false)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index 7bfeb7275..5e679768b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -110,7 +110,7 @@ namespace Barotrauma public SettingValue Difficulty; public SettingValue StartItemSet; - public CampaignSettings CreateSettings() + public readonly CampaignSettings CreateSettings() { return new CampaignSettings(element: null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index 40737a179..b093b494d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -210,8 +210,17 @@ namespace Barotrauma { CreateCustomizeWindow(CurrentSettings, settings => { + CampaignSettings prevSettings = CurrentSettings; CurrentSettings = settings; - UpdateSubList(SubmarineInfo.SavedSubmarines); + if (prevSettings.InitialMoney != settings.InitialMoney) + { + object selectedData = subList.SelectedData; + UpdateSubList(SubmarineInfo.SavedSubmarines); + if (selectedData is SubmarineInfo selectedSub && selectedSub.Price <= CurrentSettings.InitialMoney) + { + subList.Select(selectedData); + } + } }); return true; } @@ -519,7 +528,7 @@ namespace Barotrauma subsToShow = submarines.Where(s => s.IsCampaignCompatibleIgnoreClass && Path.GetDirectoryName(Path.GetFullPath(s.FilePath)) != downloadFolder).ToList(); } - subsToShow.Sort((s1, s2) => + subsToShow.Sort((s1, s2) => { int p1 = s1.Price > CurrentSettings.InitialMoney ? 10 : 0; int p2 = s2.Price > CurrentSettings.InitialMoney ? 10 : 0; @@ -537,7 +546,7 @@ namespace Barotrauma ToolTip = sub.Description, UserData = sub }; - + if (!sub.RequiredContentPackagesInstalled) { textBlock.TextColor = Color.Lerp(textBlock.TextColor, Color.DarkRed, .5f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index ed93ceee0..3d026e56a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -513,18 +513,18 @@ namespace Barotrauma var moduleLabel = new GUITextBlock(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(70 * GUI.Scale))), TextManager.Get("submarinetype.outpostmodules"), font: GUIStyle.SubHeadingFont); outpostParamsEditor.AddCustomContent(moduleLabel, 100); - foreach (KeyValuePair moduleCount in outpostGenerationParams.ModuleCounts) + foreach (var moduleCount in outpostGenerationParams.ModuleCounts) { var moduleCountGroup = new GUILayoutGroup(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(25 * GUI.Scale))), isHorizontal: true, childAnchor: Anchor.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), TextManager.Capitalize(moduleCount.Key.Value), textAlignment: Alignment.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), TextManager.Capitalize(moduleCount.Identifier.Value), textAlignment: Alignment.CenterLeft); new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), NumberType.Int) { MinValueInt = 0, MaxValueInt = 100, - IntValue = moduleCount.Value, + IntValue = moduleCount.Count, OnValueChanged = (numInput) => { - outpostGenerationParams.SetModuleCount(moduleCount.Key, numInput.IntValue); + outpostGenerationParams.SetModuleCount(moduleCount.Identifier, numInput.IntValue); if (numInput.IntValue == 0) { outpostParamsList.Select(outpostParamsList.SelectedData); @@ -540,7 +540,7 @@ namespace Barotrauma var addModuleCountGroup = new GUILayoutGroup(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(40 * GUI.Scale))), isHorizontal: true, childAnchor: Anchor.Center); HashSet availableFlags = new HashSet(); - foreach (Identifier flag in OutpostGenerationParams.OutpostParams.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } + foreach (Identifier flag in OutpostGenerationParams.OutpostParams.SelectMany(p => p.ModuleCounts.Select(m => m.Identifier))) { availableFlags.Add(flag); } foreach (var sub in SubmarineInfo.SavedSubmarines) { if (sub.OutpostModuleInfo == null) { continue; } @@ -551,7 +551,7 @@ namespace Barotrauma text: TextManager.Get("leveleditor.addmoduletype")); foreach (Identifier flag in availableFlags) { - if (outpostGenerationParams.ModuleCounts.Any(mc => mc.Key == flag)) { continue; } + if (outpostGenerationParams.ModuleCounts.Any(mc => mc.Identifier == flag)) { continue; } moduleTypeDropDown.AddItem(TextManager.Capitalize(flag.Value), flag); } moduleTypeDropDown.OnSelected += (_, userdata) => diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 442ea1bae..4ba25491a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -1549,6 +1549,11 @@ namespace Barotrauma MapEntity.DeselectAll(); ClearUndoBuffer(); + GameMain.DebugDraw = false; + GameMain.LightManager.LightingEnabled = true; + Hull.EditWater = false; + Hull.EditFire = false; + SetMode(Mode.Default); SoundPlayer.OverrideMusicType = Identifier.Empty; @@ -1586,7 +1591,7 @@ namespace Barotrauma private void CreateDummyCharacter() { - if (dummyCharacter != null) RemoveDummyCharacter(); + if (dummyCharacter != null) { RemoveDummyCharacter(); } dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false); dummyCharacter.Info.Name = "Galldren"; @@ -1876,21 +1881,15 @@ namespace Barotrauma && MainSub.Info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) { prevSavePath = MainSub.Info.FilePath.CleanUpPath(); - string prevDir = Path.GetDirectoryName(MainSub.Info.FilePath).CleanUpPath(); - - ModProject modProject = new ModProject { Name = name }; - string fileListPath = null; - ContentPackage contentPackage = GetLocalPackageThatOwnsSub(MainSub.Info); - if (contentPackage != null) + if (contentPackage == null) { - modProject = new ModProject(contentPackage); - fileListPath = contentPackage.Path; - packageToSaveTo = contentPackage; + throw new InvalidOperationException($"Tried to overwrite a submarine ({name}) that's not in a local package!"); } - - savePath = Path.Combine(prevDir, savePath).CleanUpPath(); - addSubAndSaveModProject(modProject, savePath, fileListPath ?? Path.Combine(Path.GetDirectoryName(savePath), ContentPackage.FileListFileName)); + ModProject modProject = new ModProject(contentPackage); + packageToSaveTo = contentPackage; + savePath = prevSavePath; + addSubAndSaveModProject(modProject, savePath, contentPackage.Path); } else { @@ -2074,8 +2073,8 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), outpostModuleGroup.RectTransform), TextManager.Get("outpostmoduletype"), textAlignment: Alignment.CenterLeft); HashSet availableFlags = new HashSet(); - foreach (Identifier flag in OutpostGenerationParams.OutpostParams.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } - foreach (Identifier flag in RuinGeneration.RuinGenerationParams.RuinParams.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } + foreach (Identifier flag in OutpostGenerationParams.OutpostParams.SelectMany(p => p.ModuleCounts.Select(m => m.Identifier))) { availableFlags.Add(flag); } + foreach (Identifier flag in RuinGeneration.RuinGenerationParams.RuinParams.SelectMany(p => p.ModuleCounts.Select(m => m.Identifier))) { availableFlags.Add(flag); } foreach (var sub in SubmarineInfo.SavedSubmarines) { if (sub.OutpostModuleInfo == null) { continue; } @@ -2551,11 +2550,9 @@ namespace Barotrauma { MainSub.Info.BeaconStationInfo ??= new BeaconStationInfo(MainSub.Info); } - previewImageButtonHolder.Children.ForEach(c => c.Enabled = type != SubmarineType.OutpostModule); + previewImageButtonHolder.Children.ForEach(c => c.Enabled = MainSub.Info.AllowPreviewImage); outpostSettingsContainer.Visible = type == SubmarineType.OutpostModule; - beaconSettingsContainer.Visible = type == SubmarineType.BeaconStation; - subSettingsContainer.Visible = type == SubmarineType.Player; return true; }; @@ -2572,6 +2569,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), previewImageButtonHolder.RectTransform), TextManager.Get("SubPreviewImageCreate"), style: "GUIButtonSmall") { + Enabled = MainSub?.Info.AllowPreviewImage ?? false, OnClicked = (btn, userdata) => { using (System.IO.MemoryStream imgStream = new System.IO.MemoryStream()) @@ -2589,6 +2587,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), previewImageButtonHolder.RectTransform), TextManager.Get("SubPreviewImageBrowse"), style: "GUIButtonSmall") { + Enabled = MainSub?.Info.AllowPreviewImage ?? false, OnClicked = (btn, userdata) => { FileSelection.OnFileSelected = (file) => diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index d514f6129..50edb5235 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -220,7 +220,7 @@ namespace Barotrauma }; } - private string Percentage(float v) => $"{Round(v * 100)}%"; + private string Percentage(float v) => TextManager.GetWithVariable("percentageformat", "[value]", Round(v * 100).ToString()).Value; private int Round(float v) => (int)MathF.Round(v); @@ -647,6 +647,8 @@ namespace Barotrauma { unsavedConfig.InventoryKeyMap = GameSettings.Config.InventoryKeyMapping.GetDefault(); unsavedConfig.KeyMap = GameSettings.Config.KeyMapping.GetDefault(); + Create(mainFrame.Parent.RectTransform); + Instance?.SelectTab(Tab.Controls); return true; } }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index d4e1648a3..2a71f2bd7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -253,12 +253,12 @@ namespace Barotrauma if (flipHorizontal) { float diff = targetSize.X % (sourceRect.Width * scale.X); - flippedDrawOffset.X = (int)((sourceRect.Width * scale.X - diff) / scale.X); + flippedDrawOffset.X = (int)MathF.Round((sourceRect.Width * scale.X - diff) / scale.X); } if (flipVertical) { float diff = targetSize.Y % (sourceRect.Height * scale.Y); - flippedDrawOffset.Y = (int)((sourceRect.Height * scale.Y - diff) / scale.Y); + flippedDrawOffset.Y = (int)MathF.Round((sourceRect.Height * scale.Y - diff) / scale.Y); } drawOffset += flippedDrawOffset; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs index 59624f305..27689b385 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs @@ -562,10 +562,10 @@ namespace Barotrauma.Steam = (parentList == enabledRegularModsList, parentList.AllSelected.Count > 1); Identifier swapLabel = (labelConditions switch { - (true, true) => "EnableSelectedWorkshopMods", - (true, false) => "EnableWorkshopMod", - (false, true) => "DisableSelectedWorkshopMods", - (false, false) => "DisableWorkshopMod" + (false, true) => "EnableSelectedWorkshopMods", + (false, false) => "EnableWorkshopMod", + (true, true) => "DisableSelectedWorkshopMods", + (true, false) => "DisableWorkshopMod" }).ToIdentifier(); contextMenuOptions.Add(new ContextMenuOption(swapLabel, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs index 7daabaacc..b739b3149 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs @@ -595,6 +595,12 @@ namespace Barotrauma.Steam { ContentPackageManager.WorkshopPackages.Refresh(); ContentPackageManager.EnabledPackages.RefreshUpdatedMods(); + var package = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id); + if (package is RegularPackage regular) + { + ContentPackageManager.EnabledPackages.EnableRegular(regular); + } + PopulateInstalledModLists(forceRefreshEnabled: true); }); return false; } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 812630849..7e5c42815 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.6.0 + 0.18.7.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 3907f2088..8c28ff5dd 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.6.0 + 0.18.7.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 138834ca7..6db93f270 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.6.0 + 0.18.7.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 6abd0ba2e..133bbd9d4 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.6.0 + 0.18.7.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index fa7a15b47..961807808 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.6.0 + 0.18.7.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index f2fde6748..197c22a26 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -10,6 +10,11 @@ namespace Barotrauma { private readonly Dictionary prevSentSkill = new Dictionary(); + /// + /// The client opted to create a new character and discard this one + /// + public bool Discarded; + partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel) { if (Character == null || Character.Removed) { return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index a562798b2..ae5c4e510 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -230,7 +230,7 @@ namespace Barotrauma if (!matchingCharacterData.HasSpawned) { continue; } characterInfo ??= matchingCharacterData.CharacterInfo; } - if (characterInfo == null) { continue; } + if (characterInfo == null || characterInfo.Discarded) { continue; } //reduce skills if the character has died if (characterInfo.CauseOfDeath != null && characterInfo.CauseOfDeath.Type != CauseOfDeathType.Disconnected) { @@ -420,6 +420,8 @@ namespace Barotrauma { discardedCharacters.Add(data); } + DebugConsole.Log($"Client \"{client}\" discarded the character ({data.Name})"); + data.CharacterInfo.Discarded = true; characterData.Remove(data); IncrementLastUpdateIdForFlag(NetFlags.CharacterInfo); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/ReadyCheck.cs index dea506acf..3ac8dd6a9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/ReadyCheck.cs @@ -1,4 +1,5 @@ #nullable enable +using System; using System.Collections.Generic; using System.Linq; using Barotrauma.Networking; @@ -18,7 +19,8 @@ namespace Barotrauma IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte) ServerPacketHeader.READY_CHECK); msg.Write((byte) ReadyCheckState.Start); - msg.Write(endTime); + msg.Write(new DateTimeOffset(startTime).ToUnixTimeSeconds()); + msg.Write(new DateTimeOffset(endTime).ToUnixTimeSeconds()); msg.Write(author); if (sender != null) @@ -53,10 +55,9 @@ namespace Barotrauma foreach (Client client in ActivePlayers) { IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte) ServerPacketHeader.READY_CHECK); - msg.Write((byte) ReadyCheckState.Update); - msg.Write(time); // sync time - msg.Write((byte) state); + msg.Write((byte)ServerPacketHeader.READY_CHECK); + msg.Write((byte)ReadyCheckState.Update); + msg.Write((byte)state); msg.Write(otherClient); GameMain.Server.ServerPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs index f2249351b..125557284 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs @@ -1,13 +1,22 @@ using Barotrauma.Networking; -using Microsoft.Xna.Framework; -using System; -using System.Globalization; -using System.Xml.Linq; namespace Barotrauma.Items.Components { partial class Pump : Powered, IServerSerializable, IClientSerializable { + const float NetworkUpdateInterval = 5.0f; + private float networkUpdateTimer; + + partial void UpdateProjSpecific(float deltaTime) + { + networkUpdateTimer -= deltaTime; + if (networkUpdateTimer <= 0.0f) + { + item.CreateServerEvent(this); + networkUpdateTimer = NetworkUpdateInterval; + } + } + public void ServerEventRead(IReadMessage msg, Client c) { float newFlowPercentage = msg.ReadRangedInteger(-10, 10) * 10.0f; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index 024b68518..9efc0c125 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -2,10 +2,7 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; using System.Linq; -using Barotrauma.Extensions; -using Barotrauma.MapCreatures.Behavior; namespace Barotrauma { @@ -39,9 +36,9 @@ namespace Barotrauma return; } - statusUpdateTimer -= deltaTime; - decalUpdateTimer -= deltaTime; - backgroundSectionUpdateTimer -= deltaTime; + statusUpdateTimer += deltaTime; + decalUpdateTimer += deltaTime; + backgroundSectionUpdateTimer += deltaTime; //update client hulls if the amount of water has changed by >10% //or if oxygen percentage has changed by 5% @@ -49,33 +46,32 @@ namespace Barotrauma (Math.Abs(lastSentVolume - waterVolume) > Volume * 0.1f || Math.Abs(lastSentOxygen - OxygenPercentage) > 5f || lastSentFireCount != FireSources.Count) - && statusUpdateTimer <= 0.0f; + && (statusUpdateTimer > NetConfig.HullUpdateInterval); - if (shouldSendStatusUpdate) + //force an update every 5 seconds even if nothing's changed (in case a client's gotten out of sync somehow) + if (shouldSendStatusUpdate || statusUpdateTimer > NetConfig.SparseHullUpdateInterval) { - GameMain.NetworkMember.CreateEntityEvent(this, new StatusEventData()); - + GameMain.NetworkMember.CreateEntityEvent(this, new StatusEventData()); lastSentVolume = waterVolume; lastSentOxygen = OxygenPercentage; lastSentFireCount = FireSources.Count; - - statusUpdateTimer = NetConfig.SparseHullUpdateInterval; + statusUpdateTimer = 0; } - if (decalUpdatePending && decalUpdateTimer <= 0.0f) + if (decalUpdatePending && decalUpdateTimer > NetConfig.HullUpdateInterval) { GameMain.NetworkMember.CreateEntityEvent(this, new DecalEventData()); - decalUpdateTimer = NetConfig.HullUpdateInterval; + decalUpdateTimer = 0; decalUpdatePending = false; } - if (pendingSectionUpdates.Count > 0 && backgroundSectionUpdateTimer <= 0.0f) + if (pendingSectionUpdates.Count > 0 && backgroundSectionUpdateTimer > NetConfig.HullUpdateInterval) { foreach (int pendingSectionUpdate in pendingSectionUpdates) { GameMain.NetworkMember.CreateEntityEvent(this, new BackgroundSectionsEventData(pendingSectionUpdate)); } - backgroundSectionUpdateTimer = NetConfig.HullUpdateInterval; + backgroundSectionUpdateTimer = 0; pendingSectionUpdates.Clear(); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index ed9a1efa2..fe720421c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -73,6 +73,9 @@ namespace Barotrauma.Networking characterInfo = value; } } + + public string PendingName; + public NetworkConnection Connection { get; set; } public bool SpectateOnly; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index bc9ae28ab..f9085de45 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -1046,7 +1046,7 @@ namespace Barotrauma.Networking c.LastRecvChatMsgID = NetIdUtils.Clamp(inc.ReadUInt16(), c.LastRecvChatMsgID, c.LastChatMsgQueueID); c.LastRecvClientListUpdate = NetIdUtils.Clamp(inc.ReadUInt16(), c.LastRecvClientListUpdate, LastClientListUpdateID); - TryChangeClientName(c, inc); + ReadClientNameChange(c, inc); c.LastRecvCampaignSave = inc.ReadUInt16(); if (c.LastRecvCampaignSave > 0) @@ -2675,7 +2675,7 @@ namespace Barotrauma.Networking base.AddChatMessage(message); } - private bool TryChangeClientName(Client c, IReadMessage inc) + private bool ReadClientNameChange(Client c, IReadMessage inc) { UInt16 nameId = inc.ReadUInt16(); string newName = inc.ReadString(); @@ -2685,16 +2685,21 @@ namespace Barotrauma.Networking if (c == null || string.IsNullOrEmpty(newName) || !NetIdUtils.IdMoreRecent(nameId, c.NameID)) { return false; } c.NameID = nameId; - newName = Client.SanitizeName(newName); if (newName == c.Name && newJob == c.PreferredJob && newTeam == c.PreferredTeam) { return false; } c.PreferredJob = newJob; c.PreferredTeam = newTeam; + return TryChangeClientName(c, newName); + } + + public bool TryChangeClientName(Client c, string newName) + { + newName = Client.SanitizeName(newName); //update client list even if the name cannot be changed to the one sent by the client, //so the client will be informed what their actual name is LastClientListUpdateID++; - if (newName == c.Name) { return false; } + if (newName == c.Name || string.IsNullOrEmpty(newName)) { return false; } if (IsNameValid(c, newName)) { @@ -2710,6 +2715,7 @@ namespace Barotrauma.Networking } } + private bool IsNameValid(Client c, string newName) { newName = Client.SanitizeName(newName); @@ -3542,6 +3548,10 @@ namespace Barotrauma.Networking { newName = sender.Name; } + else + { + sender.PendingName = newName; + } } int tagCount = message.ReadByte(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 28d1b91c0..889820327 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -392,6 +392,14 @@ namespace Barotrauma.Networking bool forceSpawnInMainSub = false; if (!bot && campaign != null) { + //the client has opted to change the name of their new character + //when the character spawns, set the client's name to match + if (clients[i].PendingName == characterInfos[i].Name) + { + GameMain.Server?.TryChangeClientName(clients[i], clients[i].PendingName); + clients[i].PendingName = null; + } + var matchingData = campaign?.GetClientCharacterData(clients[i]); if (matchingData != null) { diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index f8b909704..1005239b1 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.6.0 + 0.18.7.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index b760a2b2a..bc2005a63 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -227,9 +227,13 @@ namespace Barotrauma { mainLimb = Limbs.FirstOrDefault(l => IsValid(l)); } + if (mainLimb == null) + { + DebugConsole.ThrowError("Couldn't find a valid main limb. The limb can't be hidden nor be set to ignore collisions!"); + mainLimb = Limbs.FirstOrDefault(); + } } - - bool IsValid(Limb limb) => limb != null && !limb.IsSevered && !limb.IgnoreCollisions && !limb.Hidden; + static bool IsValid(Limb limb) => limb != null && !limb.IsSevered && !limb.IgnoreCollisions && !limb.Hidden; return mainLimb; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index 8d17092d1..70f6a7b61 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -49,6 +49,9 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public Identifier SpawnPointTag { get; set; } + [Serialize(CharacterTeamType.FriendlyNPC, IsPropertySaveable.Yes)] + public CharacterTeamType Team { get; protected set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Should we spawn the entity even when no spawn points with matching tags were found?")] public bool RequireSpawnPointTag { get; set; } @@ -119,7 +122,7 @@ namespace Barotrauma { if (newCharacter == null) { return; } newCharacter.HumanPrefab = humanPrefab; - newCharacter.TeamID = CharacterTeamType.FriendlyNPC; + newCharacter.TeamID = Team; newCharacter.EnableDespawn = false; humanPrefab.GiveItems(newCharacter, newCharacter.Submarine); if (LootingIsStealing) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/ReadyCheck.cs index a1b6bed04..689569ff4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/ReadyCheck.cs @@ -13,12 +13,26 @@ namespace Barotrauma internal partial class ReadyCheck { - private readonly float endTime; - private float time; + private readonly DateTime endTime; + private readonly DateTime startTime; public readonly Dictionary Clients; public bool IsFinished = false; - public ReadyCheck(List clients, float duration = 30) + public ReadyCheck(List clients, DateTime startTime, DateTime endTime) + : this(clients) + { + this.startTime = startTime; + this.endTime = endTime; + } + + public ReadyCheck(List clients, float duration) + : this(clients) + { + startTime = DateTime.Now; + endTime = startTime + new TimeSpan(0, 0, 0, 0, (int)(duration * 1000)); + } + + private ReadyCheck(List clients) { Clients = new Dictionary(); foreach (byte client in clients) @@ -27,24 +41,17 @@ namespace Barotrauma Clients.Add(client, ReadyStatus.Unanswered); } - - time = duration; - endTime = duration; -#if CLIENT - lastSecond = (int) Math.Ceiling(duration); -#endif } partial void EndReadyCheck(); public void Update(float deltaTime) { - if (time > 0) + if (DateTime.Now < endTime) { #if CLIENT UpdateBar(); #endif - time -= deltaTime; return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index bd0e5f161..fc0d44ebb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -1,10 +1,9 @@ -using Barotrauma.Networking; +using Barotrauma.MapCreatures.Behavior; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Globalization; using System.Linq; -using System.Xml.Linq; -using Barotrauma.MapCreatures.Behavior; namespace Barotrauma.Items.Components { @@ -24,7 +23,10 @@ namespace Barotrauma.Items.Components if (value == hijacked) { return; } hijacked = value; #if SERVER - item.CreateServerEvent(this); + if (!Submarine.Unloading) + { + item.CreateServerEvent(this); + } #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index 79e8295cf..f0dbf6fcb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -68,7 +68,7 @@ namespace Barotrauma.Items.Components get { return poweredList; } } - public static readonly List ChangedConnections = new List(); + public static readonly HashSet ChangedConnections = new HashSet(); public readonly static Dictionary Grids = new Dictionary(); @@ -158,6 +158,12 @@ namespace Barotrauma.Items.Components } } + /// + /// Essentially Voltage / MinVoltage (= how much of the minimum required voltage has been satisfied), clamped between 0 and 1. + /// Can be used by status effects or sounds to check if the item has enough power to run + /// + public float RelativeVoltage => minVoltage <= 0.0f ? 1.0f : MathHelper.Clamp(Voltage / minVoltage, 0.0f, 1.0f); + public bool PoweredByTinkering { get; set; } [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the item be damaged by electomagnetic pulses.")] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index 34256fe0c..6d3fbc207 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -170,6 +170,7 @@ namespace Barotrauma.Items.Components public void SetRecipientsDirty() { recipientsDirty = true; + if (IsPower) { Powered.ChangedConnections.Add(this); } } private void RefreshRecipients() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index ed95cba99..ba2e0b3d3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -399,6 +399,7 @@ namespace Barotrauma.MapCreatures.Behavior new XAttribute("pos", XMLExtensions.Vector2ToString(branch.Position)), new XAttribute("ID", branch.ID), new XAttribute("isroot", branch.IsRoot), + new XAttribute("isrootgrowth", branch.IsRootGrowth), new XAttribute("health", branch.Health.ToString("G", CultureInfo.InvariantCulture)), new XAttribute("maxhealth", branch.MaxHealth.ToString("G", CultureInfo.InvariantCulture)), new XAttribute("sides", (int)branch.Sides), @@ -457,9 +458,16 @@ namespace Barotrauma.MapCreatures.Behavior foreach ((BallastFloraBranch branch, int parentBranchId) in branches) { - if (parentBranchId > -1 && parentBranchId < Branches.Count) + if (parentBranchId > -1) { - branch.ParentBranch = Branches[parentBranchId]; + if (parentBranchId < Branches.Count) + { + branch.ParentBranch = Branches[parentBranchId]; + } + else + { + DebugConsole.AddWarning($"Error while loading ballast flora: parent branch ID {parentBranchId} out of range (total {Branches.Count} branches)"); + } } } @@ -476,6 +484,7 @@ namespace Barotrauma.MapCreatures.Behavior { Vector2 pos = branchElement.GetAttributeVector2("pos", Vector2.Zero); bool isRoot = branchElement.GetAttributeBool("isroot", false); + bool isRootGrowth = branchElement.GetAttributeBool("isrootgrowth", false); int flowerConfig = getInt("flowerconfig"); int leafconfig = getInt("leafconfig"); int id = getInt("ID"); @@ -493,7 +502,8 @@ namespace Barotrauma.MapCreatures.Behavior MaxHealth = maxhealth, Sides = (TileSide) sides, BlockedSides = (TileSide) blockedSides, - IsRoot = isRoot + IsRoot = isRoot, + IsRootGrowth = isRootGrowth }; branches.Add((newBranch, parentBranchId)); @@ -658,11 +668,14 @@ namespace Barotrauma.MapCreatures.Behavior toBeRemoved.Clear(); foreach (BallastFloraBranch branch in Branches) { - if (branch.ParentBranch == null || branch.ParentBranch.DisconnectedFromRoot || branch.ParentBranch.Health <= 0.0f) + if (!branch.IsRoot) { - float parentHealth = branch.ParentBranch == null ? 0.0f : branch.ParentBranch.Health / branch.ParentBranch.MaxHealth; - float speed = MathHelper.Lerp(5.0f, 0.1f, parentHealth); - DamageBranch(branch, speed * speed * deltaTime, AttackType.CutFromRoot); + if (branch.ParentBranch == null || branch.ParentBranch.DisconnectedFromRoot || branch.ParentBranch.Health <= 0.0f) + { + float parentHealth = branch.ParentBranch == null ? 0.0f : branch.ParentBranch.Health / branch.ParentBranch.MaxHealth; + float speed = MathHelper.Lerp(5.0f, 0.1f, parentHealth); + DamageBranch(branch, speed * speed * deltaTime, AttackType.CutFromRoot); + } } if (branch.Health <= 0.0f) { @@ -1197,7 +1210,10 @@ namespace Barotrauma.MapCreatures.Behavior } }); #if SERVER - CreateNetworkMessage(new InfectEventData(item, InfectEventData.InfectState.No, null)); + if (!item.Removed && Parent != null && !Parent.Removed) + { + CreateNetworkMessage(new InfectEventData(item, InfectEventData.InfectState.No, null)); + } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 9e550b39b..5bdabf7cf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -215,16 +215,19 @@ namespace Barotrauma saveElement = element }; - if (!string.IsNullOrWhiteSpace(levelSeed) && levelData != null && - levelData.Seed != levelSeed && !linkedSub.purchasedLostShuttles) - { - linkedSub.loadSub = false; - } - else + bool levelMatches = string.IsNullOrWhiteSpace(levelSeed) || levelData == null || levelData.Seed == levelSeed; + + //don't load a sub that was left in this level if we have a submarine switch pending + //to make sure it gets ignored during the submarine switch and item transfer (reloading and saving it during the switch makes it not considered "left behind") + if ((levelMatches || linkedSub.purchasedLostShuttles) && GameMain.GameSession?.Campaign?.PendingSubmarineSwitch == null) { linkedSub.loadSub = true; linkedSub.rect.Location = MathUtils.ToPoint(pos); } + else + { + linkedSub.loadSub = false; + } } #warning TODO: revise @@ -279,14 +282,14 @@ namespace Barotrauma if (worldPos != Vector2.Zero) { if (GameMain.GameSession != null && GameMain.GameSession.MirrorLevel) - { + { worldPos.X = GameMain.GameSession.LevelData.Size.X - worldPos.X; } sub.SetPosition(worldPos); } else { - sub.SetPosition(WorldPosition); + sub.SetPosition(WorldPosition); } DockingPort linkedPort = null; @@ -308,8 +311,17 @@ namespace Barotrauma { linkedPort = (FindEntityByID(originalLinkedToID) as Item)?.GetComponent(); } - if (linkedPort == null) { return; } } + + if (linkedPort == null) + { + if (worldPos == Vector2.Zero) + { + DebugConsole.ThrowError("Something went wrong when loading a linked submarine - the save didn't include either a world position or a linked port for the submarine."); + } + return; + } + originalLinkedPort = linkedPort; ushort originalMyId = childRemap.GetOffsetId(originalMyPortID); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index 6d75e6916..4c65efaa3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -1,11 +1,9 @@ using Barotrauma.Extensions; -using Microsoft.Xna.Framework; using System; using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -98,9 +96,29 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes), Editable] public string ReplaceInRadiation { get; set; } - private readonly Dictionary moduleCounts = new Dictionary(); + public class ModuleCount + { + public Identifier Identifier; + public int Count; + public int Order; - public IReadOnlyDictionary ModuleCounts + public ModuleCount(ContentXElement element) + { + Identifier = element.GetAttributeIdentifier("flag", element.GetAttributeIdentifier("moduletype", "")); + Count = element.GetAttributeInt("count", 0); + Order = element.GetAttributeInt("order", 0); + } + + public ModuleCount(Identifier id, int count) + { + Identifier = id; + Count = count; + } + } + + private readonly List moduleCounts = new List(); + + public IReadOnlyList ModuleCounts { get { return moduleCounts; } } @@ -171,8 +189,7 @@ namespace Barotrauma switch (subElement.Name.ToString().ToLowerInvariant()) { case "modulecount": - Identifier moduleFlag = subElement.GetAttributeIdentifier("flag", subElement.GetAttributeIdentifier("moduletype", "")); - moduleCounts[moduleFlag] = subElement.GetAttributeInt("count", 0); + moduleCounts.Add(new ModuleCount(subElement)); break; case "npcs": var newCollection = new NpcCollection(); @@ -200,7 +217,7 @@ namespace Barotrauma public int GetModuleCount(Identifier moduleFlag) { if (moduleFlag == Identifier.Empty || moduleFlag == "none") { return int.MaxValue; } - return moduleCounts.ContainsKey(moduleFlag) ? moduleCounts[moduleFlag] : 0; + return moduleCounts.FirstOrDefault(m => m.Identifier == moduleFlag)?.Count ?? 0; } public void SetModuleCount(Identifier moduleFlag, int count) @@ -208,11 +225,19 @@ namespace Barotrauma if (moduleFlag == Identifier.Empty || moduleFlag == "none") { return; } if (count <= 0) { - moduleCounts.Remove(moduleFlag); + moduleCounts.RemoveAll(m => m.Identifier == moduleFlag); } else { - moduleCounts[moduleFlag] = count; + var moduleCount = moduleCounts.FirstOrDefault(m => m.Identifier == moduleFlag); + if (moduleCount == null) + { + moduleCounts.Add(new ModuleCount(moduleFlag, count)); + } + else + { + moduleCount.Count = count; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 1da36a5fb..a0e5ceed6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -99,7 +99,7 @@ namespace Barotrauma { //if the module doesn't have the ruin flag or any other flag used in the generation params, don't use it in ruins if (!subInfo.OutpostModuleInfo.ModuleFlags.Contains("ruin".ToIdentifier()) && - !generationParams.ModuleCounts.Any(m => subInfo.OutpostModuleInfo.ModuleFlags.Contains(m.Key))) + !generationParams.ModuleCounts.Any(m => subInfo.OutpostModuleInfo.ModuleFlags.Contains(m.Identifier))) { continue; } @@ -141,16 +141,11 @@ namespace Barotrauma selectedModules.Clear(); //select which module types the outpost should consist of - List pendingModuleFlags; - using (var md5 = MD5.Create()) - { - #warning TODO: cursed - pendingModuleFlags = onlyEntrance - ? generationParams.ModuleCounts - .Keys.OrderBy(k => ToolBox.IdentifierToUint32Hash(k, md5)) - .First().ToEnumerable().ToList() - : SelectModules(outpostModules, generationParams); - } + List pendingModuleFlags = + onlyEntrance ? + generationParams.ModuleCounts.First().Identifier.ToEnumerable().ToList() : + SelectModules(outpostModules, generationParams); + foreach (Identifier flag in pendingModuleFlags) { if (flag == "none") { continue; } @@ -437,31 +432,27 @@ namespace Barotrauma var pendingModuleFlags = new List(); bool availableModulesFound = true; - Identifier initialModuleFlag = generationParams.ModuleCounts.FirstOrDefault().Key; + Identifier initialModuleFlag = generationParams.ModuleCounts.FirstOrDefault().Identifier; pendingModuleFlags.Add(initialModuleFlag); while (pendingModuleFlags.Count < totalModuleCount && availableModulesFound) { availableModulesFound = false; foreach (var moduleFlag in generationParams.ModuleCounts) { - if (pendingModuleFlags.Count(m => m == moduleFlag.Key) >= generationParams.GetModuleCount(moduleFlag.Key)) + if (pendingModuleFlags.Count(m => m == moduleFlag.Identifier) >= generationParams.GetModuleCount(moduleFlag.Identifier)) { continue; } - if (!modules.Any(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag.Key))) + if (!modules.Any(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag.Identifier))) { - DebugConsole.ThrowError($"Failed to add a module to the outpost (no modules with the flag \"{moduleFlag.Key}\" found)."); + DebugConsole.ThrowError($"Failed to add a module to the outpost (no modules with the flag \"{moduleFlag.Identifier}\" found)."); continue; } availableModulesFound = true; - pendingModuleFlags.Add(moduleFlag.Key); + pendingModuleFlags.Add(moduleFlag.Identifier); } } - using (MD5 md5 = MD5.Create()) - { - pendingModuleFlags.Sort((i1, i2) => (int)ToolBox.StringToUInt32Hash(i1.Value.ToLowerInvariant(), md5) - (int)ToolBox.StringToUInt32Hash(i2.Value.ToLowerInvariant(), md5)); - } - pendingModuleFlags.Shuffle(Rand.RandSync.ServerAndClient); + pendingModuleFlags.OrderBy(f => generationParams.ModuleCounts.First(m => m.Identifier == f)).ThenBy(f => Rand.Value(Rand.RandSync.ServerAndClient)); while (pendingModuleFlags.Count < totalModuleCount) { //don't place "none" modules at the end because @@ -610,7 +601,7 @@ namespace Barotrauma Identifier flagToPlace = "none".ToIdentifier(); SubmarineInfo nextModule = null; - foreach (Identifier moduleFlag in pendingModuleFlags) + foreach (Identifier moduleFlag in pendingModuleFlags.OrderByDescending(f => currentModule?.Info?.OutpostModuleInfo.AllowAttachToModules.Contains(f) ?? false)) { flagToPlace = moduleFlag; nextModule = GetRandomModule(currentModule?.Info?.OutpostModuleInfo, availableModules, flagToPlace, gapPosition, locationType, allowDifferentLocationType); @@ -1048,6 +1039,17 @@ namespace Barotrauma module.ThisGapPosition == OutpostModuleInfo.GapPosition.Left || module.ThisGapPosition == OutpostModuleInfo.GapPosition.Right; + if (!module.ThisGap.linkedTo.Any()) + { + DebugConsole.ThrowError($"Error during outpost generation: {module.ThisGapPosition} gap in module \"{module.Info.Name}\" was not linked to any hulls."); + continue; + } + if (!module.PreviousGap.linkedTo.Any()) + { + DebugConsole.ThrowError($"Error during outpost generation: {GetOpposingGapPosition(module.ThisGapPosition)} gap in module \"{module.PreviousModule.Info.Name}\" was not linked to any hulls."); + continue; + } + MapEntity leftHull = module.ThisGap.Position.X < module.PreviousGap.Position.X ? module.ThisGap.linkedTo[0] : module.PreviousGap.linkedTo[0]; MapEntity rightHull = module.ThisGap.Position.X > module.PreviousGap.Position.X ? module.ThisGap.linkedTo.Count == 1 ? module.ThisGap.linkedTo[0] : module.ThisGap.linkedTo[1] : diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index 3d1ca403e..1e37ab37e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -109,6 +109,8 @@ namespace Barotrauma public bool IsCampaignCompatible => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus) && SubmarineClass != SubmarineClass.Undefined; public bool IsCampaignCompatibleIgnoreClass => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus); + public bool AllowPreviewImage => Type == SubmarineType.Player; + public Md5Hash MD5Hash { get @@ -556,7 +558,7 @@ namespace Barotrauma XDocument doc = new XDocument(newElement); doc.Root.Add(new XAttribute("name", Name)); - if (previewImage != null) + if (previewImage != null && AllowPreviewImage) { doc.Root.Add(new XAttribute("previewimage", Convert.ToBase64String(previewImage.ToArray()))); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 097f1c888..9e3417054 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -619,6 +619,11 @@ namespace Barotrauma if (parentObject is Powered powered) { value = powered.Voltage; return true; } } break; + case nameof(Powered.RelativeVoltage): + { + if (parentObject is Powered powered) { value = powered.RelativeVoltage; return true; } + } + break; case nameof(Powered.CurrPowerConsumption): { if (parentObject is Powered powered) { value = powered.CurrPowerConsumption; return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index f510d30da..c95171170 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -480,6 +480,8 @@ namespace Barotrauma bool voiceCaptureChanged = currentConfig.Audio.VoiceCaptureDevice != newConfig.Audio.VoiceCaptureDevice; bool textScaleChanged = Math.Abs(currentConfig.Graphics.TextScale - newConfig.Graphics.TextScale) > MathF.Pow(2.0f, -7); + bool hudScaleChanged = !MathUtils.NearlyEqual(currentConfig.Graphics.HUDScale, newConfig.Graphics.HUDScale); + bool setGraphicsMode = resolutionChanged || currentConfig.Graphics.VSync != newConfig.Graphics.VSync || @@ -514,6 +516,10 @@ namespace Barotrauma componentStyle.RefreshSize(); } } + if (hudScaleChanged) + { + HUDLayoutSettings.CreateAreas(); + } GameMain.SoundManager?.ApplySettings(); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index e165d24e7..9922dc37f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -100,13 +100,23 @@ namespace Barotrauma } #endif - if (Path.IsPathRooted(originalFilename)) + string startPath = directory ?? ""; + + string saveFolder = SaveUtil.SaveFolder.Replace('\\', '/'); + if (originalFilename.Replace('\\', '/').StartsWith(saveFolder)) { + //paths that lead to the save folder might have incorrect case, + //mainly if they come from a filelist + startPath = saveFolder.EndsWith('/') ? saveFolder : $"{saveFolder}/"; + filename = startPath; + subDirs = subDirs.Skip(saveFolder.Split('/').Length).ToArray(); + } + else if (Path.IsPathRooted(originalFilename)) + { + #warning TODO: incorrect assumption or...? Figure out what this was actually supposed to fix, if anything. Might've been a perf thing. return originalFilename; //assume that rooted paths have correct case since these are generated by the game } - string startPath = directory ?? ""; - for (int i = 0; i < subDirs.Length; i++) { if (i == subDirs.Length - 1 && string.IsNullOrEmpty(subDirs[i])) diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 3d5d91ced..0a155659b 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,41 @@ +--------------------------------------------------------------------------------------------------------- +v0.18.7.0 +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Reduced depth charge scale to fit in loader/racks better. +- Fixed ballast flora dying by itself client-side when transitioning to a new level. +- Disallow laying in vertically flipped normal bunks. +- Fixed broken door sprite stretching on the wrong axis when cutting a door. +- Fixed AI orders not working in multiplayer. +- Fixed tutorial characters starting with more items than they should. +- Fixed chat box closing when clicking any chat box elements after opening it with the keybind. +- Fixed submarine selection resetting when changing server settings in the campaign setup menu. +- Fixed "enable" and "disable" being wrong way around in the mod tab's context menus. +- Fixed sub editor always saving subs from a local mod in the root of that mod's folder instead of the actual location of that sub file. +- Fixed decoys not being containable in wrecked coilgun ammo shelves. +- Fixes and improvements to the new beacon stations and the enemies inside them. +- Fixed an issue during submarine switching when a drone / linked sub has been left behind (Regalis11/Barotrauma#9172): When you were switching back to a submarine whose drone you'd left behind, in the same level where you left the drone, the drone would get loaded and resaved during the item transfer, which caused it to no longer be considered "left behind". It also removed the drone's position from the save, and since it was not docked to anything either, positioning it failed at the beginning of the next round. + +Changes: +- Adding preview images to wrecks, beacon stations, outposts or enemy subs isn't allowed in the sub editor (unnecessarily bloats up their file size, as the preview images aren't visible anywhere). + +Fixes: +- Fixed server not refreshing the power grid when a client disconnects and reconnects a power wire. +- Fixed hull updates not being sent if the water/oxygen/fire in the hull doesn't change server-side, preventing the hull's status from getting corrected if a client somehow ends up out of sync. +- Fixed keybinds shown in the controls tab not refreshing when resetting the binds. +- Hopefully fixed colonies sometimes not including some modules (most often the armory module). +- Fixed ready checks sometimes ending at a slightly different time client-side compared to the server, allowing you to answer the prompt even though the time to answer already ended server-side. +- Fixed large terminal welcome messages going slightly outside the bounds of the listbox. +- Fixed overlapping in the tab menu's mission tab when there's more than one mission selected. +- Fixed fabricators and deconstructors playing the sounds even if they're out of power. +- Fixed occasional "hash mismatch for downloaded mod" errors on Linux. +- Fixed clients occasionally spawning as the old character after they've opted to create a new one. Only happened if the client hadn't died and was still controlling the old character at the end of the round. +- When a client creates a character with a new name, the client's name is changed to match it after they spawn as that character. +- Fixed enabled mods getting disabled when updating them in the mods menu. +- Fixed a rounding error in Sprite.DrawTiled that sometimes caused an extra 1-pixel line on some scaled and flipped structures (e.g. certain wall pieces scaled to 0.6). +- Fixed Orca 2 still using the old chaingun charge time. + --------------------------------------------------------------------------------------------------------- v0.18.6.0 --------------------------------------------------------------------------------------------------------- From bf4a5ff73c50310c2e027da44b88d3277c661300 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Sat, 11 Jun 2022 13:46:49 +0300 Subject: [PATCH 09/14] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 216c02877..a02641add 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -52,9 +52,9 @@ body: label: Version description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu. options: - - 0.17.15.0 - - 0.18.6.0 (Unstable) - - 0.18.5.0 (Unstable) + - 0.17.16.0 + - 0.18.8.0 (Unstable) + - 0.18.7.0 (Unstable) - Other validations: required: true From e38bfeb1cc7cc01db7fa7ed8801f065a1efc6a82 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Mon, 13 Jun 2022 18:39:50 +0300 Subject: [PATCH 10/14] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a02641add..2443947dc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -53,8 +53,8 @@ body: description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu. options: - 0.17.16.0 + - 0.18.9.0 (Unstable) - 0.18.8.0 (Unstable) - - 0.18.7.0 (Unstable) - Other validations: required: true From 856f894203f247611735929f62883762c1ddf2a3 Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Tue, 14 Jun 2022 04:14:47 +0900 Subject: [PATCH 11/14] Build 0.18.9.0 --- .../Events/Missions/AlienRuinMission.cs | 2 - .../ClientSource/GUI/GUICanvas.cs | 29 +- .../ClientSource/GUI/GUIStyle.cs | 16 + .../BarotraumaClient/ClientSource/GameMain.cs | 10 +- .../GameModes/MultiPlayerCampaign.cs | 5 +- .../GameModes/Tutorials/ScenarioTutorial.cs | 5 +- .../Map/Creatures/BallastFloraBehavior.cs | 13 + .../ClientSource/Networking/GameClient.cs | 7 + .../ClientSource/Screens/CampaignUI.cs | 12 +- .../ClientSource/Screens/MainMenuScreen.cs | 1 + .../ClientSource/Screens/SubEditorScreen.cs | 5 +- .../ClientSource/Steam/BulkDownloader.cs | 4 +- .../WorkshopMenu/Mutable/InstalledTab.cs | 6 +- .../Steam/WorkshopMenu/Mutable/ItemList.cs | 14 +- .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../ServerSource/Networking/RespawnManager.cs | 2 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../AI/Objectives/AIObjectivePumpWater.cs | 1 + .../SharedSource/DebugConsole.cs | 10 + .../SharedSource/Events/EventManager.cs | 8 + .../SharedSource/Events/EventSet.cs | 20 +- .../Events/Missions/AlienRuinMission.cs | 11 +- .../Events/Missions/CargoMission.cs | 142 ++++--- .../SharedSource/Events/ScriptedEvent.cs | 1 - .../GameSession/GameModes/CampaignMode.cs | 2 + .../SharedSource/GameSession/GameSession.cs | 4 +- .../Items/Components/ItemContainer.cs | 40 +- .../Items/Components/Projectile.cs | 4 + .../Map/Creatures/BallastFloraBehavior.cs | 26 +- .../SharedSource/Map/LinkedSubmarine.cs | 37 +- .../SharedSource/Screens/GameScreen.cs | 5 +- .../Serialization/SerializableProperty.cs | 2 +- .../SharedSource/Settings/GameSettings.cs | 17 +- .../SharedSource/Utils/TaskPool.cs | 10 +- Barotrauma/BarotraumaShared/changelog.txt | 353 ++++++------------ 39 files changed, 459 insertions(+), 377 deletions(-) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AlienRuinMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AlienRuinMission.cs index dbfd01ecc..f4a737254 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AlienRuinMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AlienRuinMission.cs @@ -20,14 +20,12 @@ namespace Barotrauma if (targetId == Entity.NullEntityID) { continue; } Entity target = Entity.FindEntityByID(targetId); if (target == null) { continue; } - existingTargets.Add(target); allTargets.Add(target); } ushort spawnedTargetsCount = msg.ReadUInt16(); for (int i = 0; i < spawnedTargetsCount; i++) { var enemy = Character.ReadSpawnData(msg); - existingTargets.Add(enemy); allTargets.Add(enemy); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs index c555ce055..d27e2c086 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs @@ -8,6 +8,8 @@ namespace Barotrauma { public class GUICanvas : RectTransform { + private static readonly object mutex = new object(); + protected GUICanvas() : base(size, parent: null) { } private static GUICanvas _instance; @@ -39,22 +41,25 @@ namespace Barotrauma private static void OnChildrenChanged(RectTransform _) { - //add weak reference if we don't have one yet - foreach (var child in _instance.Children) + lock (mutex) { - if (!_instance.childrenWeakRef.Any(c => c.TryGetTarget(out var existingChild) && existingChild == child)) + //add weak reference if we don't have one yet + foreach (var child in _instance.Children) { - _instance.childrenWeakRef.Add(new WeakReference(child)); + if (!_instance.childrenWeakRef.Any(c => c.TryGetTarget(out var existingChild) && existingChild == child)) + { + _instance.childrenWeakRef.Add(new WeakReference(child)); + } } - } - //get rid of strong references - _instance.children.Clear(); - //remove dead children - for (int i = _instance.childrenWeakRef.Count - 2; i >= 0; i--) - { - if (!_instance.childrenWeakRef[i].TryGetTarget(out var child) || child.Parent != _instance) + //get rid of strong references + _instance.children.Clear(); + //remove dead children + for (int i = _instance.childrenWeakRef.Count - 2; i >= 0; i--) { - _instance.childrenWeakRef.RemoveAt(i); + if (!_instance.childrenWeakRef[i].TryGetTarget(out var child) || child.Parent != _instance) + { + _instance.childrenWeakRef.RemoveAt(i); + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index d81ee4206..27be2752f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -207,5 +207,21 @@ namespace Barotrauma return ItemQualityColorNormal; } } + + public static void RecalculateFonts() + { + foreach (var font in Fonts.Values) + { + font.Prefabs.ForEach(p => p.LoadFont()); + } + } + + public static void RecalculateSizeRestrictions() + { + foreach (var componentStyle in ComponentStyles) + { + componentStyle.RefreshSize(); + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 7f55a1968..6a926c4da 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -282,9 +282,9 @@ namespace Barotrauma screen.OnFileDropped(filePath, extension); } - public void ApplyGraphicsSettings() + public void ApplyGraphicsSettings(bool recalculateFontsAndStyles = false) { - void updateConfig() + static void updateConfig() { var config = GameSettings.CurrentConfig; config.Graphics.Width = GraphicsWidth; @@ -323,6 +323,12 @@ namespace Barotrauma defaultViewport = GraphicsDevice.Viewport; + if (recalculateFontsAndStyles) + { + GUIStyle.RecalculateFonts(); + GUIStyle.RecalculateSizeRestrictions(); + } + ResolutionChanged?.Invoke(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 41d89b0d5..8fc431ad8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -745,7 +745,8 @@ namespace Barotrauma purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall)); } - if (ShouldApply(NetFlags.UpgradeManager, id, requireUpToDateSave: true)) + if (!Submarine.Unloading && !(Submarine.MainSub is { Loading: true }) && + ShouldApply(NetFlags.UpgradeManager, id, requireUpToDateSave: true)) { UpgradeStore.WaitForServerUpdate = false; campaign.UpgradeManager.SetPendingUpgrades(pendingUpgrades); @@ -761,7 +762,7 @@ namespace Barotrauma campaign.UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall, force: true); } } - foreach (Item item in Item.ItemList) + foreach (Item item in Item.ItemList.ToList()) { if (item.PendingItemSwap != null && !purchasedItemSwaps.Any(it => it.ItemToRemove == item)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs index 01142cabe..7d72179b1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs @@ -1,10 +1,7 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; -using System; using System.Collections.Generic; -using Barotrauma.IO; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Tutorials { @@ -228,6 +225,8 @@ namespace Barotrauma.Tutorials { CoroutineManager.StopCoroutines(tutorialCoroutine); } + GUI.PreventPauseMenuToggle = false; + ContentRunning = false; infoBox = null; } else if (Character.Controlled.IsDead) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs index 264b5b2e7..0fc5d2c4f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs @@ -183,8 +183,21 @@ namespace Barotrauma.MapCreatures.Behavior Color branchColor = (branch.IsRoot || branch.IsRootGrowth) ? RootColor : Color.White; + if (GameMain.DebugDraw) { + if (branch.DisconnectedFromRoot && branch.ParentBranch == null) + { + branchColor = Color.Yellow; + } + else if (branch.DisconnectedFromRoot) + { + branchColor = Color.Cyan; + } + else if (branch.ParentBranch == null) + { + branchColor = Color.Magenta; + } #if DEBUG Vector2 basePos = Parent.WorldPosition; foreach (var (from, to) in debugSearchLines) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 2419901fd..fdb7451f0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -1696,6 +1696,13 @@ namespace Barotrauma.Networking } } + if (clientPeer == null) + { + DebugConsole.ThrowError("There was an error initializing the round (disconnected during the StartGame coroutine.)"); + roundInitStatus = RoundInitStatus.Error; + yield return CoroutineStatus.Failure; + } + roundInitStatus = RoundInitStatus.WaitingForStartGameFinalize; DateTime? timeOut = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 6cc459e40..3d0a30141 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -526,7 +526,7 @@ namespace Barotrauma tickBox.OnSelected += (GUITickBox tb) => { if (!Campaign.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap)) { return false; } - + if (tb.Selected) { Campaign.Map.CurrentLocation.SelectMission(mission); @@ -549,7 +549,7 @@ namespace Barotrauma { GameMain.Client?.SendCampaignState(); } - return true; + return true; }; missionTickBoxes.Add(tickBox); @@ -731,6 +731,14 @@ namespace Barotrauma if (submarineSelection == null) submarineSelection = new SubmarineSelection(false, () => Campaign.ShowCampaignUI = false, tabs[(int)CampaignMode.InteractionType.PurchaseSub].RectTransform); submarineSelection.RefreshSubmarineDisplay(true, setTransferOptionToTrue: true); break; + case CampaignMode.InteractionType.Map: + //refresh mission rewards (may have been changed by e.g. a pending submarine switch) + foreach (GUITextBlock rewardText in missionRewardTexts) + { + Mission mission = (Mission)rewardText.UserData; + rewardText.Text = mission.GetMissionRewardText(Submarine.MainSub); + } + break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 14748954f..88691410e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -74,6 +74,7 @@ namespace Barotrauma { CreateHostServerFields(); CreateCampaignSetupUI(); + SettingsMenu.Create(menuTabs[Tab.Settings].RectTransform); if (remoteContentDoc?.Root != null) { remoteContentContainer.ClearChildren(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 4ba25491a..61a57a7ef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -1549,10 +1549,7 @@ namespace Barotrauma MapEntity.DeselectAll(); ClearUndoBuffer(); - GameMain.DebugDraw = false; - GameMain.LightManager.LightingEnabled = true; - Hull.EditWater = false; - Hull.EditFire = false; + DebugConsole.DeactivateCheats(); SetMode(Mode.Default); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs index b8f6328f8..ddac26c05 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs @@ -119,9 +119,11 @@ namespace Barotrauma.Steam onComplete?.Invoke(); } msgBox.Close(); + ContentPackageManager.WorkshopPackages.Refresh(); + ContentPackageManager.EnabledPackages.RefreshUpdatedMods(); if (SettingsMenu.Instance?.WorkshopMenu is MutableWorkshopMenu mutableWorkshopMenu) { - mutableWorkshopMenu.PopulateInstalledModLists(); + mutableWorkshopMenu.PopulateInstalledModLists(forceRefreshEnabled: true); } }); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs index 27689b385..2e5953ae5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs @@ -589,7 +589,7 @@ namespace Barotrauma.Steam isEnabled: true, onSelected: () => { - TaskPool.Add($"UnsubFromSelected", Task.WhenAll(selectedMods.Select(m => SteamManager.Workshop.GetItem(m.SteamWorkshopId))), + TaskPool.AddIfNotFound($"UnsubFromSelected", Task.WhenAll(selectedMods.Select(m => SteamManager.Workshop.GetItem(m.SteamWorkshopId))), t => { if (!t.TryGetResult(out Steamworks.Ugc.Item?[] items)) { return; } @@ -668,7 +668,7 @@ namespace Barotrauma.Steam { infoButton.Enabled = false; } - TaskPool.Add( + TaskPool.AddIfNotFound( $"DetermineUpdateRequired{mod.SteamWorkshopId}", mod.IsUpToDate(), t => @@ -709,7 +709,7 @@ namespace Barotrauma.Steam addRegularModsToList(enabledMods, enabledRegularModsList); if (refreshDisabled) { addRegularModsToList(disabledMods, disabledRegularModsList); } - TaskPool.Add( + TaskPool.AddIfNotFound( $"DetermineWorkshopModIcons", SteamManager.Workshop.GetPublishedItems(), t => diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs index b739b3149..e4e155950 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs @@ -418,7 +418,7 @@ namespace Barotrauma.Steam itemListBox.ClearChildren(); itemListBox.Deselect(); itemListBox.ScrollBar.BarScroll = 0.0f; - TaskPool.Add("PopulateTabWithItemList", items, + TaskPool.AddIfNotFound("PopulateTabWithItemList", items, (t) => { taskCancelSrc = taskCancelSrc.IsCancellationRequested ? new CancellationTokenSource() : taskCancelSrc; @@ -591,16 +591,16 @@ namespace Barotrauma.Steam bool reinstallAction(GUIButton button, object o) { - TaskPool.Add($"Reinstall{workshopItem.Id}", SteamManager.Workshop.Reinstall(workshopItem), t => + int prevIndex = ContentPackageManager.EnabledPackages.Regular.IndexOf(contentPackage); + TaskPool.AddIfNotFound($"Reinstall{workshopItem.Id}", + SteamManager.Workshop.Reinstall(workshopItem), t => { ContentPackageManager.WorkshopPackages.Refresh(); ContentPackageManager.EnabledPackages.RefreshUpdatedMods(); - var package = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id); - if (package is RegularPackage regular) + if (SettingsMenu.Instance?.WorkshopMenu is MutableWorkshopMenu mutableWorkshopMenu) { - ContentPackageManager.EnabledPackages.EnableRegular(regular); + mutableWorkshopMenu.PopulateInstalledModLists(forceRefreshEnabled: true); } - PopulateInstalledModLists(forceRefreshEnabled: true); }); return false; } @@ -615,7 +615,7 @@ namespace Barotrauma.Steam if (contentPackage != null) { - TaskPool.Add( + TaskPool.AddIfNotFound( $"DetermineUpdateRequired{contentPackage.SteamWorkshopId}", contentPackage.IsUpToDate(), t => diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 7e5c42815..dcb6b28f4 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.7.0 + 0.18.9.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 8c28ff5dd..dfc188b15 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.7.0 + 0.18.9.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 6db93f270..8043dbfaf 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.7.0 + 0.18.9.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 133bbd9d4..26ca8d0d6 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.7.0 + 0.18.9.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 961807808..883360337 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.7.0 + 0.18.9.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 889820327..de685bebf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -390,7 +390,7 @@ namespace Barotrauma.Networking characterInfos[i].ClearCurrentOrders(); bool forceSpawnInMainSub = false; - if (!bot && campaign != null) + if (!bot) { //the client has opted to change the name of their new character //when the character spawns, set the client's name to match diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 1005239b1..e1bf7a611 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.7.0 + 0.18.9.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs index 6377ef8a5..eb232b4f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -26,6 +26,7 @@ namespace Barotrauma protected override bool Filter(Pump pump) { + if (pump?.Item == null || pump.Item.Removed) { return false; } if (pump.Item.IgnoreByAI(character)) { return false; } if (!pump.Item.IsInteractable(character)) { return false; } if (pump.IsAutoControlled) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 3722751b0..0b4ac4c1d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -2526,5 +2526,15 @@ namespace Barotrauma ThrowError("Saving debug console log to " + filePath + " failed", e); } } + + public static void DeactivateCheats() + { +#if CLIENT + GameMain.DebugDraw = false; + GameMain.LightManager.LightingEnabled = true; +#endif + Hull.EditWater = false; + Hull.EditFire = false; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 94b7d0c01..63dd365ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -408,6 +408,14 @@ namespace Barotrauma bool isPrefabSuitable(EventPrefab e) => e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier; + + foreach (var subEventPrefab in eventSet.EventPrefabs) + { + 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}\"."); + } + } var suitablePrefabSubsets = eventSet.EventPrefabs.Where( e => e.EventPrefabs.Any(isPrefabSuitable)).ToArray(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index ebd615008..3c2be4201 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -142,11 +142,15 @@ namespace Barotrauma { foreach (var id in (Identifier[])PrefabOrIdentifier) { - yield return EventPrefab.Prefabs[id]; + if (EventPrefab.Prefabs.TryGet(id, out EventPrefab prefab)) + { + yield return prefab; + } } } } } + public readonly float? SelfCommonness; public float Commonness => SelfCommonness ?? EventPrefabs.MaxOrNull(p => p.Commonness) ?? 0.0f; @@ -159,6 +163,20 @@ namespace Barotrauma commonness = Commonness; probability = Probability; } + + public IEnumerable GetMissingIdentifiers() + { + if (PrefabOrIdentifier.TryCast(out var ids)) + { + foreach (var id in ids) + { + if (!EventPrefab.Prefabs.ContainsKey(id)) + { + yield return id; + } + } + } + } } public readonly ImmutableArray EventPrefabs; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs index 78ddfaed8..2ed692fa5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs @@ -125,10 +125,6 @@ namespace Barotrauma if (!AllTargetsEliminated()) { return; } State = 1; break; - case 1: - if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; } - State = 2; - break; } } @@ -166,11 +162,16 @@ namespace Barotrauma public override void End() { - if (State == 2) + bool exitingLevel = GameMain.GameSession?.GameMode is CampaignMode campaign ? + campaign.GetAvailableTransition() != CampaignMode.TransitionType.None : + Submarine.MainSub is { } sub && (sub.AtEndExit || sub.AtStartExit); + + if (State > 0 && exitingLevel) { GiveReward(); completed = true; } + failed = !completed && State > 0; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index 8a02928df..58aae000c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -3,7 +3,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -23,15 +22,16 @@ namespace Barotrauma private int calculatedReward; private int maxItemCount; - private Submarine sub; - + private Submarine currentSub; + private SubmarineInfo nextRoundSubInfo; + private readonly List previouslySelectedMissions = new List(); public override LocalizedString Description { get { - if (Submarine.MainSub != sub) + if ((GameMain.GameSession?.Campaign?.PendingSubmarineSwitch ?? Submarine.MainSub?.Info) != nextRoundSubInfo) { string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(Submarine.MainSub))}‖end‖"; if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } @@ -43,7 +43,8 @@ namespace Barotrauma public CargoMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { - this.sub = sub; + this.currentSub = sub; + this.nextRoundSubInfo = sub?.Info; itemConfig = prefab.ConfigElement.GetChildElement("Items"); requiredDeliveryAmount = Math.Min(prefab.ConfigElement.GetAttributeFloat("requireddeliveryamount", 0.98f), 1.0f); //this can get called between rounds when the client receives a campaign save @@ -57,39 +58,13 @@ namespace Barotrauma private void DetermineCargo() { - if (this.sub == null || itemConfig == null) + if (this.currentSub == null || itemConfig == null) { calculatedReward = Prefab.Reward; return; } itemsToSpawn.Clear(); - List<(ItemContainer container, int freeSlots)> containers = sub.GetCargoContainers(); - containers.Sort((c1, c2) => { return c2.container.Capacity.CompareTo(c1.container.Capacity); }); - - previouslySelectedMissions.Clear(); - if (GameMain.GameSession?.StartLocation?.SelectedMissions != null) - { - bool isPriorMission = true; - foreach (Mission mission in GameMain.GameSession.StartLocation.SelectedMissions) - { - if (!(mission is CargoMission otherMission)) { continue; } - if (mission == this) { isPriorMission = false; } - previouslySelectedMissions.Add(otherMission); - if (!isPriorMission) { continue; } - foreach (var (element, container) in otherMission.itemsToSpawn) - { - for (int i = 0; i < containers.Count; i++) - { - if (containers[i].container == container) - { - containers[i] = (containers[i].container, containers[i].freeSlots - 1); - break; - } - } - } - } - } maxItemCount = 0; foreach (var subElement in itemConfig.Elements()) @@ -98,18 +73,85 @@ namespace Barotrauma maxItemCount += maxCount; } - for (int i = 0; i < containers.Count; i++) + var pendingSubInfo = GameMain.GameSession?.Campaign?.PendingSubmarineSwitch; + if (pendingSubInfo != null && pendingSubInfo != currentSub.Info) { - foreach (var subElement in itemConfig.Elements()) + //if we've got a submarine switch pending, calculate the amount of cargo based on it's cargo capacity + //TODO: this isn't guaranteed to be accurate, because we don't take existing items in the new sub's cargo containers + //or items that might get transferred in them into account + maxItemCount = Math.Min(maxItemCount, pendingSubInfo.CargoCapacity); + previouslySelectedMissions.Clear(); + if (GameMain.GameSession?.StartLocation?.SelectedMissions != null) { - int maxCount = subElement.GetAttributeInt("maxcount", 10); - if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { continue; } - ItemPrefab itemPrefab = FindItemPrefab(subElement); - while (containers[i].freeSlots > 0 && containers[i].container.Inventory.CanBePut(itemPrefab)) + bool isPriorMission = true; + foreach (Mission mission in GameMain.GameSession.StartLocation.SelectedMissions) { - containers[i] = (containers[i].container, containers[i].freeSlots - 1); - itemsToSpawn.Add((subElement, containers[i].container)); - if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { break; } + if (!(mission is CargoMission otherMission)) { continue; } + if (mission == this) { isPriorMission = false; } + previouslySelectedMissions.Add(otherMission); + if (!isPriorMission) { continue; } + maxItemCount -= otherMission.itemsToSpawn.Count; + } + } + for (int i = 0; i < maxItemCount; i++) + { + foreach (var subElement in itemConfig.Elements()) + { + int maxCount = subElement.GetAttributeInt("maxcount", 10); + if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { continue; } + ItemPrefab itemPrefab = FindItemPrefab(subElement); + while (itemsToSpawn.Count < maxItemCount) + { + itemsToSpawn.Add((subElement, null)); + if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { break; } + } + } + } + maxItemCount = Math.Max(0, maxItemCount); + nextRoundSubInfo = pendingSubInfo; + } + else + { + List<(ItemContainer container, int freeSlots)> containers = currentSub.GetCargoContainers(); + containers.Sort((c1, c2) => { return c2.container.Capacity.CompareTo(c1.container.Capacity); }); + + previouslySelectedMissions.Clear(); + if (GameMain.GameSession?.StartLocation?.SelectedMissions != null) + { + bool isPriorMission = true; + foreach (Mission mission in GameMain.GameSession.StartLocation.SelectedMissions) + { + if (!(mission is CargoMission otherMission)) { continue; } + if (mission == this) { isPriorMission = false; } + previouslySelectedMissions.Add(otherMission); + if (!isPriorMission) { continue; } + foreach (var (element, container) in otherMission.itemsToSpawn) + { + for (int i = 0; i < containers.Count; i++) + { + if (containers[i].container == container) + { + containers[i] = (containers[i].container, containers[i].freeSlots - 1); + break; + } + } + } + } + } + + for (int i = 0; i < containers.Count; i++) + { + foreach (var subElement in itemConfig.Elements()) + { + int maxCount = subElement.GetAttributeInt("maxcount", 10); + if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { continue; } + ItemPrefab itemPrefab = FindItemPrefab(subElement); + while (containers[i].freeSlots > 0 && containers[i].container.Inventory.CanBePut(itemPrefab)) + { + containers[i] = (containers[i].container, containers[i].freeSlots - 1); + itemsToSpawn.Add((subElement, containers[i].container)); + if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { break; } + } } } } @@ -135,7 +177,7 @@ namespace Barotrauma } if (rewardPerCrate.HasValue && rewardPerCrate < 0) { rewardPerCrate = null; } - string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))}‖end‖"; + string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(currentSub))}‖end‖"; if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } } @@ -167,18 +209,26 @@ namespace Barotrauma } } } - - if (sub != this.sub || missionsChanged) + + var pendingSubInfo = GameMain.GameSession?.Campaign?.PendingSubmarineSwitch; + if (pendingSubInfo != null && nextRoundSubInfo != pendingSubInfo) { - this.sub = sub; + this.nextRoundSubInfo = pendingSubInfo; DetermineCargo(); } + else if (sub != this.currentSub || missionsChanged) + { + this.currentSub = sub; + this.nextRoundSubInfo = sub.Info; + DetermineCargo(); + } + return calculatedReward; } private void InitItems() { - this.sub = Submarine.MainSub; + this.currentSub = Submarine.MainSub; DetermineCargo(); items.Clear(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index 8054a79ac..cbb841a31 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index ea2e4ed68..9edca9159 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -507,6 +507,8 @@ namespace Barotrauma } } + public TransitionType GetAvailableTransition() => GetAvailableTransition(out _, out _); + /// /// Which submarine is at a position where it can leave the level and enter another one (if any). /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index fe1f4fad2..68bfe6969 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -622,8 +622,6 @@ namespace Barotrauma return; } - var originalSubPos = Submarine.WorldPosition; - if (level.StartOutpost != null) { //start by placing the sub below the outpost @@ -706,7 +704,7 @@ namespace Barotrauma if (!ls.LoadSub || ls.Sub.DockedTo.Contains(Submarine)) { continue; } if (Submarine.Info.LeftBehindDockingPortIDs.Contains(ls.OriginalLinkedToID)) { continue; } if (ls.Sub.Info.SubmarineElement.Attribute("location") != null) { continue; } - ls.Sub.SetPosition(ls.Sub.WorldPosition + (Submarine.WorldPosition - originalSubPos)); + ls.SetPositionRelativeToMainSub(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 58050a7ab..2b0e7db1d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -30,7 +30,7 @@ namespace Barotrauma.Items.Components class SlotRestrictions { public readonly int MaxStackSize; - public readonly List ContainableItems; + public List ContainableItems; public SlotRestrictions(int maxStackSize, List containableItems) { @@ -187,7 +187,7 @@ namespace Barotrauma.Items.Components [Serialize(false, IsPropertySaveable.No)] public bool RemoveContainedItemsOnDeconstruct { get; set; } - private ImmutableArray slotRestrictions; + private readonly ImmutableArray slotRestrictions; readonly List targets = new List(); @@ -215,21 +215,13 @@ namespace Barotrauma.Items.Components public override bool RecreateGUIOnResolutionChange => true; - public List ContainableItems { get; private set; } + public List ContainableItems { get; } public ItemContainer(Item item, ContentXElement element) : base(item, element) - { - LoadContainableRestrictions(element); - InitProjSpecific(element); - } - - public void LoadContainableRestrictions(ContentXElement element) { int totalCapacity = capacity; - ContainableItems?.Clear(); - foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -250,7 +242,7 @@ namespace Barotrauma.Items.Components } } Inventory = new ItemInventory(item, this, totalCapacity, SlotsPerRow); - + List newSlotRestrictions = new List(totalCapacity); for (int i = 0; i < capacity; i++) { @@ -261,7 +253,7 @@ namespace Barotrauma.Items.Components foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().ToLowerInvariant() != "subcontainer") { continue; } - + int subCapacity = subElement.GetAttributeInt("capacity", 1); int subMaxStackSize = subElement.GetAttributeInt("maxstacksize", maxStackSize); @@ -289,6 +281,28 @@ namespace Barotrauma.Items.Components capacity = totalCapacity; slotRestrictions = newSlotRestrictions.ToImmutableArray(); System.Diagnostics.Debug.Assert(totalCapacity == slotRestrictions.Length); + InitProjSpecific(element); + } + + public void ReloadContainableRestrictions(ContentXElement element) + { + int containableIndex = 0; + foreach (var subElement in element.GetChildElements("containable")) + { + 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."); + continue; + } + ContainableItems[containableIndex] = containable; + containableIndex++; + if (containableIndex >= ContainableItems.Count) { break; } + } + for (int i = 0; i < capacity; i++) + { + slotRestrictions[i].ContainableItems = ContainableItems; + } } public int GetMaxStackSize(int slotIndex) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index b72ea07b8..7ca7eff7d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -846,6 +846,10 @@ namespace Barotrauma.Items.Components } else if (target.Body.UserData is Limb limb) { + if (!FriendlyFire && User != null && limb.character.IsFriendly(User)) + { + return false; + } // when hitting limbs with piercing ammo, don't lose as much speed if (MaxTargetsToHit > 1) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index ba2e0b3d3..2a961dcd8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -460,13 +460,14 @@ namespace Barotrauma.MapCreatures.Behavior { if (parentBranchId > -1) { - if (parentBranchId < Branches.Count) + var parentBranch = Branches.Find(b => b.ID == parentBranchId); + if (parentBranch == null) { - branch.ParentBranch = Branches[parentBranchId]; + DebugConsole.AddWarning($"Error while loading ballast flora: couldn't find a parent branch with the ID {parentBranchId}"); } else { - DebugConsole.AddWarning($"Error while loading ballast flora: parent branch ID {parentBranchId} out of range (total {Branches.Count} branches)"); + branch.ParentBranch = parentBranch; } } } @@ -790,7 +791,8 @@ namespace Barotrauma.MapCreatures.Behavior MaxHealth = RootHealth, Health = RootHealth, IsRoot = true, - CurrentHull = Parent + CurrentHull = Parent, + ID = CreateID() }; Branches.Add(root); @@ -1015,14 +1017,6 @@ namespace Barotrauma.MapCreatures.Behavior public void DamageBranch(BallastFloraBranch branch, float amount, AttackType type, Character? attacker = null) { float damage = amount; - if (damage > 0) - { - damage = Math.Min(damage, branch.Health); - } - else - { - damage = Math.Max(damage, branch.Health - branch.MaxHealth); - } if (type != AttackType.Other && type != AttackType.CutFromRoot) { @@ -1081,6 +1075,14 @@ namespace Barotrauma.MapCreatures.Behavior } } + if (damage > 0) + { + damage = Math.Min(damage, branch.Health); + } + else + { + damage = Math.Max(damage, branch.Health - branch.MaxHealth); + } branch.Health -= damage; #if SERVER diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 5bdabf7cf..da8d88b14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -1,12 +1,11 @@ -using Barotrauma.Items.Components; +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; -using Barotrauma.IO; -using Barotrauma.Extensions; -using System.Collections.Immutable; namespace Barotrauma { @@ -82,6 +81,8 @@ namespace Barotrauma private XElement saveElement; + private Vector2? positionRelativeToMainSub; + public override bool Linkable { get @@ -256,6 +257,15 @@ namespace Barotrauma } } + public void SetPositionRelativeToMainSub() + { + if (positionRelativeToMainSub.HasValue) + { + Sub.SetPosition(Submarine.WorldPosition + positionRelativeToMainSub.Value); + } + positionRelativeToMainSub = null; + } + public override void OnMapLoaded() { if (!loadSub) { return; } @@ -317,7 +327,19 @@ namespace Barotrauma { if (worldPos == Vector2.Zero) { - DebugConsole.ThrowError("Something went wrong when loading a linked submarine - the save didn't include either a world position or a linked port for the submarine."); + Vector2 relativePos = saveElement.GetAttributeVector2("posrelativetomainsub", Vector2.Zero); + if (relativePos != Vector2.Zero) + { + positionRelativeToMainSub = relativePos; + } + else + { + DebugConsole.ThrowError("Something went wrong when loading a linked submarine - the save didn't include a world position, a linked port or position relative to the main sub."); + } + } + else + { + sub.Submarine = Submarine; } return; } @@ -469,8 +491,9 @@ namespace Barotrauma } else { - if (saveElement.Attribute("location") != null) saveElement.Attribute("location").Remove(); - if (saveElement.Attribute("worldpos") != null) saveElement.Attribute("worldpos").Remove(); + if (saveElement.Attribute("location") != null) { saveElement.Attribute("location").Remove(); } + if (saveElement.Attribute("worldpos") != null) { saveElement.Attribute("worldpos").Remove(); } + saveElement.SetAttributeValue("posrelativetomainsub", XMLExtensions.Vector2ToString(sub.WorldPosition - Submarine.WorldPosition)); } saveElement.SetAttributeValue("pos", XMLExtensions.Vector2ToString(Position - Submarine.HiddenSubPosition)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index eaedf8556..36743762a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -79,7 +79,6 @@ namespace Barotrauma public override void Deselect() { base.Deselect(); - #if CLIENT var config = GameSettings.CurrentConfig; config.CrewMenuOpen = CrewManager.PreferCrewMenuOpen; @@ -88,6 +87,10 @@ namespace Barotrauma GameSettings.SaveCurrentConfig(); GameMain.SoundManager.SetCategoryMuffle("default", false); GUI.ClearMessages(); + if (GameMain.GameSession?.GameMode is TestGameMode) + { + DebugConsole.DeactivateCheats(); + } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 9e3417054..0cae07ba2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -1128,7 +1128,7 @@ namespace Barotrauma if (itemComponent is ItemContainer itemContainer && (componentElement.GetChildElement("containable") != null || componentElement.GetChildElement("subcontainer") != null)) { - itemContainer.LoadContainableRestrictions(componentElement); + itemContainer.ReloadContainableRestrictions(componentElement); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index c95171170..c21422874 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -492,7 +492,11 @@ namespace Barotrauma #if CLIENT if (setGraphicsMode) { - GameMain.Instance.ApplyGraphicsSettings(); + GameMain.Instance.ApplyGraphicsSettings(recalculateFontsAndStyles: true); + } + else if (textScaleChanged) + { + GUIStyle.RecalculateFonts(); } if (audioOutputChanged) @@ -505,17 +509,6 @@ namespace Barotrauma VoipCapture.ChangeCaptureDevice(currentConfig.Audio.VoiceCaptureDevice); } - if (textScaleChanged || resolutionChanged) - { - foreach (var font in GUIStyle.Fonts.Values) - { - font.Prefabs.ForEach(p => p.LoadFont()); - } - foreach (var componentStyle in GUIStyle.ComponentStyles) - { - componentStyle.RefreshSize(); - } - } if (hudScaleChanged) { HUDLayoutSettings.CreateAreas(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskPool.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskPool.cs index 921725254..f2b0fceb6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskPool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskPool.cs @@ -40,10 +40,14 @@ namespace Barotrauma } } - private static void AddInternal(string name, Task task, Action onCompletion, object userdata) + private static void AddInternal(string name, Task task, Action onCompletion, object userdata, bool addIfFound = true) { lock (taskActions) { + if (!addIfFound) + { + if (taskActions.Any(t => t.Name == name)) { return; } + } if (taskActions.Count >= MaxTasks) { throw new Exception( @@ -59,6 +63,10 @@ namespace Barotrauma { AddInternal(name, task, (Task t, object obj) => { onCompletion?.Invoke(t); }, null); } + public static void AddIfNotFound(string name, Task task, Action onCompletion) + { + AddInternal(name, task, (Task t, object obj) => { onCompletion?.Invoke(t); }, null, addIfFound: false); + } public static void Add(string name, Task task, U userdata, Action onCompletion) where U : class { diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 0a155659b..58186d997 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,241 +1,25 @@ --------------------------------------------------------------------------------------------------------- -v0.18.7.0 +v0.18.9.0 --------------------------------------------------------------------------------------------------------- -Unstable only: -- Reduced depth charge scale to fit in loader/racks better. -- Fixed ballast flora dying by itself client-side when transitioning to a new level. -- Disallow laying in vertically flipped normal bunks. -- Fixed broken door sprite stretching on the wrong axis when cutting a door. -- Fixed AI orders not working in multiplayer. -- Fixed tutorial characters starting with more items than they should. -- Fixed chat box closing when clicking any chat box elements after opening it with the keybind. -- Fixed submarine selection resetting when changing server settings in the campaign setup menu. -- Fixed "enable" and "disable" being wrong way around in the mod tab's context menus. -- Fixed sub editor always saving subs from a local mod in the root of that mod's folder instead of the actual location of that sub file. -- Fixed decoys not being containable in wrecked coilgun ammo shelves. -- Fixes and improvements to the new beacon stations and the enemies inside them. -- Fixed an issue during submarine switching when a drone / linked sub has been left behind (Regalis11/Barotrauma#9172): When you were switching back to a submarine whose drone you'd left behind, in the same level where you left the drone, the drone would get loaded and resaved during the item transfer, which caused it to no longer be considered "left behind". It also removed the drone's position from the save, and since it was not docked to anything either, positioning it failed at the beginning of the next round. - -Changes: -- Adding preview images to wrecks, beacon stations, outposts or enemy subs isn't allowed in the sub editor (unnecessarily bloats up their file size, as the preview images aren't visible anywhere). - -Fixes: -- Fixed server not refreshing the power grid when a client disconnects and reconnects a power wire. -- Fixed hull updates not being sent if the water/oxygen/fire in the hull doesn't change server-side, preventing the hull's status from getting corrected if a client somehow ends up out of sync. -- Fixed keybinds shown in the controls tab not refreshing when resetting the binds. -- Hopefully fixed colonies sometimes not including some modules (most often the armory module). -- Fixed ready checks sometimes ending at a slightly different time client-side compared to the server, allowing you to answer the prompt even though the time to answer already ended server-side. -- Fixed large terminal welcome messages going slightly outside the bounds of the listbox. -- Fixed overlapping in the tab menu's mission tab when there's more than one mission selected. -- Fixed fabricators and deconstructors playing the sounds even if they're out of power. -- Fixed occasional "hash mismatch for downloaded mod" errors on Linux. -- Fixed clients occasionally spawning as the old character after they've opted to create a new one. Only happened if the client hadn't died and was still controlling the old character at the end of the round. -- When a client creates a character with a new name, the client's name is changed to match it after they spawn as that character. -- Fixed enabled mods getting disabled when updating them in the mods menu. -- Fixed a rounding error in Sprite.DrawTiled that sometimes caused an extra 1-pixel line on some scaled and flipped structures (e.g. certain wall pieces scaled to 0.6). -- Fixed Orca 2 still using the old chaingun charge time. +- Updated translations. +- Fixed crashing when a mod includes event sets that reference a non-existent event. +- Backwards compatibility: readded some mission events we'd removed since the previous update. --------------------------------------------------------------------------------------------------------- -v0.18.6.0 ---------------------------------------------------------------------------------------------------------- - -Unstable only: -- Fixed crashing during outpost generation. - ---------------------------------------------------------------------------------------------------------- -v0.18.5.0 ---------------------------------------------------------------------------------------------------------- - -Chat improvements: -- Chat mode (radio/local) can be toggled using a dropdown next to the chat box or with a dedicated "ToggleChatMode" keybind (R by default). -- Voice chat now has only one push-to-talk keybind (V by default) which respects the selected chat mode. -- There's now a dedicated "ActiveChat" keybind (T by default) to open the chat using the currently active chat mode. -- If you want to keep the chat keybinds the way they were (separate keybinds for local and radio), you can rebind the "Chat" and "RadioChat" inputs back to T and R and the new "ToggleChatMode" and "ActiveChat" inputs to something else. - -Changes: -- Optimized bot AIs: in particular, the cleanup, repair, pump water and load items objectives. Should significantly improve performance when the bots are doing these objectives when there's a large number of items in the sub. -- Optimized entity culling logic (determines which items/structures are currently visible in the screen). -- Optimized a bunch of textures. -- Improved the performance statistics view that's enabled with the "showperf" console command: more fine-grained stats and easier-to-read visuals. -- Added UI volume slider. -- Depth charges can be stored in coilgun ammo shelves. - -Fixes: -- Fixed ballast flora branches that have been disconnected from the root not being considered disconnected after a level transition (allowing them to keep growing). -- Fixed "set default bindings" not doing anything in the settings menu. -- Fixed door/hatch gaps not getting moved when snapping to grid in the sub editor. -- Vertically mirrored beds can't be laid on. -- Fixed wrecked reactors being forced to non-interactable even if made interactable in the sub editor. - -Unstable only: -- Fixed crashing when you try to edit beacon station settings on a station you haven't saved yet. -- Fixed level difficulties being incorrect in the "normal" difficulty setting (levels were set to the minimum difficulty of the biome, instead of a linear increase across the biome). -- Misc fixes and improvements to the new beacon stations. -- Fixed "max missions per round" value not being restricted in the UI. -- Fixed items not getting transferred to/from linked subs. - -Modding: -- Doors and hatches can now be mirrored in the sub editor (making them open from top to bottom, or from right to left). - ---------------------------------------------------------------------------------------------------------- -v0.18.4.0 ---------------------------------------------------------------------------------------------------------- - -Unstable only: -- Option to select whether to automatically transfer the items from the old sub to a new one when switching the sub. -- Fixed Physicorium Chaingun Ammunition Box having an incorrect sprite. -- Fixed crashing when trying to load a save that includes ItemContainers with different kinds of inventory slots (e.g. PUCS). -- Disposable suits no longer protect from pressure when broken. -- Made disposable suits play a sound and their lights flicker when the suit is about to break. -- Fixed inability to access the character tab when your character is dead in non-campaign modes, fixed creating a new character not doing anything mid-round. -- Fixed sprite editor crashing if you try to reload a texture twice. -- Fixed drone/shuttles getting left behind in the outpost when you buy and switch to a sub that has a one. -- Fixed crashing when closing the submarine preview window. -- Disallowed mirroring beds vertically. - -Changes: -- Added some new campaign settings: starting balance, amount of starting items and difficulty. -- Added two new beacon stations. -- Made the descriptions of some materials (that used to just say "useful for crafting") more descriptive. -- Increased oxygen generator output in some vanilla subs. -- Made handheld sonar beacon sound less grating. -- The client who initiated a vote cannot take part in that vote (except if they're the only client who can vote, in which case the vote automatically passes). -- Made flashlight flicker before the battery runs out. -- Added some lootable money to corpses found in wrecks. -- Removed the small equipment indicators next to the character portrait. -- Added a bunch of new UI sounds (tickbox toggling, confirming transactions, increase/decrease sounds for number inputs, cart sound for adding/removing items in store interfaces, selecting/clicking components, sliders and modlist). -- Optimized many status effects by making them only execute once per second instead of every frame (most importantly, diving suits and volatile fuel rods). -- Optimized talents: buffs are applied to characters periodically instead of every frame. -- Optimized the logic that bots use to determine the safety of hulls. -- Optimized items: stop updating items that don't need to be updated more aggressively. -- Weapon holders now use the tag "mountableweapon" instead of "weapon" to determine which items can be placed in them. Allows tagging non-weapon items as mountable in the holder, without making bots consider it a weapon due to the "weapon" tag. Also allows to keep some weapons not-mountable. -- Ammunition Shelf can now also store Depth Charges ("depthchargeammo" tag added) - -Fixes: -- Split campaign state networking messages into multiple ones. Previously all the campaign-related data (map state, reputation, upgrades, purchased items, selected missions) was included in the same message, and whenever anything in the data changed, the server would send all of it to clients. This would cause performance and bandwidth issues in some situations, for example when reputation was changing rapidly. -- Fixed some pumps in Kastrull working without power. -- Fixed quick-reloading working incorrectly when trying to reload from a stack that doesn't fully fit in the weapon (e.g. when double clicking on a full stack of revolver rounds with a half-loaded revolver in hand). -- Fixed inability to quick-reload weapons with more than 1 inventory slot (e.g. autoshotgun). -- Fixed outpost NPCs having x3 more health than they should. -- Fixed morbusine not killing NPCs with higher-than-default health. -- Fixed crashing with the error message "couldn't find a valid ICU package installed on the system" on some Linux distributions. -- Fixed graphics errors when using Razer Cortex overlay. -- Fixed bots being unable to repair Winterhalter's top hatch. -- Fixed server crashing if you disable all mission types and try to start a mission round. -- Fixed Chinese/Japanese/Korean text not wrapping properly on terminals. -- Fixed bots sometimes walking towards a wall or holding the ladders when they are idling. -- Fixed "main docking port" property not being taken into account when placing outposts (= the outpost was placed with the assumption that the docking port closest to the sub's center is the main docking port). Sometimes caused the outpost to be placed too close to the level walls, preventing the sub from docking with it. -- Fixed ladders not being visible in the sub preview. -- Fixed some UI elements being too large when switching from a large resolution to a smaller one, or vice versa. -- Fixed weapon holder sprite depth. -- Fixed level editor's test mode generating a different level than the editor itself. - -Modding: -- Added "mod lists" which can be used to enable/disable sets of mods more easily. -- Option to choose which local mod(s) to add a submarine to when saving one in the submarine editor. -- Mods can be unsubscribed from by right-clicking on them in the mod list, and it's possible to unsubscribe from multiple ones at the same time by using ctrl+click or shift+click to select more than one. -- Local mods can be merged in the mod list by selecting the ones you want to merge and selecting "merge all selected" from the right-click context menu. -- Better filtering in the mod list: option to only show local mods, Workshop mods, published mods, submarines and/or item assemblies. -- Added "SameInventory" spawn position type to status effects (allows spawning items in the same inventory the entity applying the effect is in). -- Added support for multiple light components in wearables. - ---------------------------------------------------------------------------------------------------------- -v0.18.3.0 ---------------------------------------------------------------------------------------------------------- - -Unstable only: -- Fixed submarine not getting saved between levels in the multiplayer campaign. -- Fixed clients sometimes unnecessarily showing the "trying to automatically dock with the outpost" prompt even when the docking is being done manually. -- Added a new texture and icon for shotgun rubber shell. -- Added sprite for disposable suit on shelf. - -Changes: -- TriggerComponent now supports negative forces: negative force value will cause the it to pull triggerers towards it. - -Modding: -- Multiple TriggerComponent properties can now be modified through signals and CustomInterface components. - ---------------------------------------------------------------------------------------------------------- -v0.18.2.0 ---------------------------------------------------------------------------------------------------------- - -Unstable only: -- Fixed "submarine equality check failed" errors in non-campaign multiplayer game modes. -- Fixed occasional crashes when exiting the sub editor. -- Fixed diving suit lights being on when not worn. -- Fixed research stations not working. -- Adjusted the layout of server settings gameplay tab to prevent overlaps on small resolutions. -- Fixed ignore orders not being loaded correctly in singleplayer. - -Changes: -- Lighting optimization: now some unimportant (dim and small) lights are hidden when there's lots of light sources visible on the screen at the same time. The maximum number of visible lights can be adjusted in the game settings. -- Lighting optimization: the number of light recalculations per frame is limited, meaning that when there's lots of moving, shadow-casting lights visible, the game doesn't try to recalculate the shadows all at the same time. -- Lighting optimization: simplify the light rendering when zoomed very far out (e.g. when looking through a periscope). -- Optimized status effects that modify items' conditions every frame (for example, oxygen tank shelves that fill up oxygen tanks). -- Hide AppData path from tooltips in the sub editor to prevent exposing the user's name. -- Reduce nausea chance of energy drink to 25%. -- Changes to the campaign progression in general. -- Changes to the level generation parameters, especially in Cold Caverns and the Ridge. -- Changes to the level resources distribution. -- Changes to the event manager settings (that affect the monster spawns). -- Adjusted and normalized the item loadouts for all the jobs. -- Changes to the items that always spawn with the sub at the beginning of the game (start items). -- Adjustments to the preferred containers (= where things are spawned and where they should be placed). -- Changes to the existing missions and how they are distributed. Added new missions. -- Reduced the costs for unlocking the biomes. -- Minor adjustments to the monster spawns. -- Changes to the item "gating". Some items don't appear early in the game anymore. -- Adjustments to the mission specific variants of the monsters. -- Added a large Crawler variant for some missions (removed the Swarmcrawler that was used for crawler missions). -- Halved Mudraptors' priority for eating dead bodies. - -Fixes: -- Fixed abyss area being very small in the Aphotic Plateau, preventing the abyss monster from reaching you if you go deep enough. -- Fixed status monitor displaying small amounts of water as 1% even though water detectors output 0%. -- Fixed autopilot conflicting with VELOCITY_IN inputs (now signals override the autopilot for 1 second). -- Fixed ConversationAction getting interrupted when opening an input-blocking menu in single player. -- Fixed sprite bleed in chaingun ammunition boxes. -- Fixed appearance of specific named NPCs being inconsistent (e.g. Captain Hognose sometimes being a woman or not having an eyepatch). -- Fixed certain scripted events getting stuck if you switch characters in single player (e.g. the events that require you to interact with fliers on the wall). -- Fixed crashing when the source of a rope is removed (e.g. when a latcher despawns while latched on to the sub). -- Fixed votes always going through if no-one votes. -- Fixed energy drink giving x10 more haste when used via the health interface. -- Fixed the monster spawns for the new game plus not working (currently a placeholder set). -- Fixed monsters spawning from missions not avoiding the engines. - -Modding: -- Level object, cave and mineral commonness can be defined based on the biome instead of the level generation parameters (= no need to define commonness for "coldcavernsbasic", "coldcavernsmaze" etc separately). -- Option to define ConversationAction texts directly in the event xml (instead of having to always define them in a spearate text file). -- Extended CustomInterface functionality with NumberInput elements that allow using float values ("numbertype") and defining the increment size ("step") the number of decimal places ("decimalplaces"). (Thanks, mLuby!) -- Implemented element for removing all the child elements of an element in a variant file. - ---------------------------------------------------------------------------------------------------------- -v0.18.1.0 ---------------------------------------------------------------------------------------------------------- - -Unstable only: -- Fixed crashing when someone respawns while the tab menu is open. -- Fixed level equality errors in multiplayer game modes other than the campaign. -- Fixed headsets from pre-0.18 saves being unusable due to still having the battery requirement but no inventory to hold one. -- Fixed inability to put disposable diving suits in diving suit lockers. -- Fixed clients without money management permissions not being able to request money. -- Fixed context menus rendering behind the debug console. - ---------------------------------------------------------------------------------------------------------- -v0.18.0.0 +v0.18.8.0 --------------------------------------------------------------------------------------------------------- Balancing: +- Added some new campaign settings: starting balance, amount of starting items and difficulty. - Cargo mission reward of construction materials has been reduced to be less balance-breaking. -- Many fabrication and deconstruction recipes changed to prevent infinite construction loops. +- Revisited all item spawns. Drastically reduced and adjusted the spawns everywhere. Disabled some spawns in campaign. All the subs should now start with a bare minimum in the campaign. - Reduced selling price to ~25% of base price to avoid getting too rich from looting too early/easily - Increased effect of "Requested Goods" to be 2x to compensate for the decreased selling price. - Alien artifacts and trinkets can still be sold for a high price at research stations. (2x modifier, to compensate for the reduced selling price) - Removed batteries from Headset, to reduce the value of selling/deconstructing these. - Duffelbag deteriorates over time when in use, and now is carried with both hands. - All items now deconstruct into less materials than it takes to construct them. Avoiding infinite construction/deconstruction loops for easy skill leveling. -- Revisited all item spawns (WIP). Drastically reduced and adjusted the spawns everywhere. Disabled some spawns in campaign. All the subs should now start with a bare minimum in the campaign. - (Temporarily?) Removed most hand-placed items from the vanilla subs to make balancing and debugging the auto item placement easier. - Revisited crew corpse spawns. The id cards are no longer manually placed. The cards found from the crew now actually work. - Minor adjustments to bandit loadouts. @@ -244,15 +28,34 @@ Balancing: - Adjusted the armor penetration of all turrets. - Made location evolution take a little longer, colonies cannot be formed closer than three steps to another colony. - Made wreck missions a little more common. +- Adjustments to the preferred containers (= where things are spawned and where they should be placed). +- Changes to the existing missions and how they are distributed. Added new missions. +- Reduced the costs for unlocking the biomes. +- Adjustments to the monster spawns. +- Changes to the item "gating". Some items don't appear early in the game anymore. +- Adjustments to the mission specific variants of the monsters. +- Added a large Crawler variant for some missions (removed the Swarmcrawler that was used for crawler missions). +- Halved Mudraptors' priority for eating dead bodies. +- Reduce nausea chance of energy drink to 25%. +- Changes to the campaign progression in general. +- Changes to the level generation parameters, especially in Cold Caverns and the Ridge. +- Changes to the level resources distribution. +- Changes to the event manager settings (that affect the monster spawns). +- Adjusted and normalized the item loadouts for all the jobs. + +Chat improvements: +- Chat mode (radio/local) can be toggled using a dropdown next to the chat box or with a dedicated "ToggleChatMode" keybind (R by default). +- Voice chat now has only one push-to-talk keybind (V by default) which respects the selected chat mode. +- There's now a dedicated "ActiveChat" keybind (T by default) to open the chat using the currently active chat mode. +- If you want to keep the chat keybinds the way they were (separate keybinds for local and radio), you can rebind the "Chat" and "RadioChat" inputs back to T and R and the new "ToggleChatMode" and "ActiveChat" inputs to something else. Changes and additions: - Added damage overlays to characters (characters who've taken damage look damaged). +- Added two new beacon stations. +- Added a bunch of new UI sounds (tickbox toggling, confirming transactions, increase/decrease sounds for number inputs, cart sound for adding/removing items in store interfaces, selecting/clicking components, sliders and modlist). +- Added UI volume slider. - Show a verification prompt if an automated circuit tries to make the submarine undock from or dock with an outpost. Prevents campaign getting softlocked if someone rewires the docking port in a way that makes it dock/undock immediately at the start of around. - Color subs in the sub editor's list to indicate whether they're vanilla, workshop or local subs, added a tooltip that explains why some of them cannot be deleted through the editor. -- Optimized AI pathfinding when they're trying to find a safe hull. Particularly noticeable in colonies when the NPCs are fleeing from something. -- Optimized character status effects (e.g. health regen and other constant damage reductions). -- Optimized watcher's acid clouds. -- Optimized loading submarines. Reduces loading times especially when there's lots of items in the sub. - ID cards can now be purchased from outposts. The card gets assigned the appropriate tags for the character doing the purchase. - Clients need to wait 1 minute if their vote gets rejected before they can start another vote of the same type. - Increased the priority of explosion particles to make it less likely for them to not appear when the particle limit has been reached. @@ -270,10 +73,41 @@ Changes and additions: - Prevented selling items from submarine containers tagged with "dontsellitems", instead of "donttakeitems". - Removed merchant balance effect on item prices. - Replaced "item sell value" with the location reputation effect on the store interface. +- Hide AppData path from tooltips in the sub editor to prevent exposing the user's name. +- Made the descriptions of some materials (that used to just say "useful for crafting") more descriptive. +- Increased oxygen generator output in some vanilla subs. +- Made handheld sonar beacon sound less grating. +- The client who initiated a vote cannot take part in that vote (except if they're the only client who can vote, in which case the vote automatically passes). +- Made flashlight flicker before the battery runs out. +- Added some lootable money to corpses found in wrecks. +- Removed the small equipment indicators next to the character portrait. +- Weapon holders now use the tag "mountableweapon" instead of "weapon" to determine which items can be placed in them. Allows tagging non-weapon items as mountable in the holder, without making bots consider it a weapon due to the "weapon" tag. Also allows to keep some weapons not-mountable. +- Ammunition Shelf can now also store Depth Charges ("depthchargeammo" tag added) +- Doors and hatches can now be mirrored in the sub editor (making them open from top to bottom, or from right to left). +- Depth charges can be stored in coilgun ammo shelves. +- Adding preview images to wrecks, beacon stations, outposts or enemy subs isn't allowed in the sub editor (unnecessarily bloats up their file size, as the preview images aren't visible anywhere). + +Performance: +- Improved the performance statistics view that's enabled with the "showperf" console command: more fine-grained stats and easier-to-read visuals. +- Optimized AI pathfinding when they're trying to find a safe hull. Particularly noticeable in colonies when the NPCs are fleeing from something. +- Optimized character status effects (e.g. health regen and other constant damage reductions). +- Optimized watcher's acid clouds. +- Optimized loading submarines. Reduces loading times especially when there's lots of items in the sub. +- Lighting optimization: now some unimportant (dim and small) lights are hidden when there's lots of light sources visible on the screen at the same time. The maximum number of visible lights can be adjusted in the game settings. +- Lighting optimization: the number of light recalculations per frame is limited, meaning that when there's lots of moving, shadow-casting lights visible, the game doesn't try to recalculate the shadows all at the same time. +- Lighting optimization: simplify the light rendering when zoomed very far out (e.g. when looking through a periscope). +- Optimized status effects that modify items' conditions every frame (for example, oxygen tank shelves that fill up oxygen tanks). +- Optimized many status effects by making them only execute once per second instead of every frame (most importantly, diving suits and volatile fuel rods). +- Optimized talents: buffs are applied to characters periodically instead of every frame. +- Optimized the logic that bots use to determine the safety of hulls. +- Optimized items: stop updating items that don't need to be updated more aggressively. +- Optimized bot AIs: in particular, the cleanup, repair, pump water and load items objectives. Should significantly improve performance when the bots are doing these objectives when there's a large number of items in the sub. +- Optimized entity culling logic (determines which items/structures are currently visible in the screen). +- Optimized a bunch of textures. Fixes: -- Fixed an issue where the client was adding mission rewards into the bank on their screen causing desync. -- Fixed item sets failing to load when the system language is set to Turkish, causing NPCs to spawn without any items. +- Fixed server not refreshing the power grid when a client disconnects and reconnects a power wire. +- Fixed hull updates not being sent if the water/oxygen/fire in the hull doesn't change server-side, preventing the hull's status from getting corrected if a client somehow ends up out of sync. - Fixed ballast flora sometimes becoming unkillable in multiplayer. - Attempt to fix tab menu crew list sometimes getting stuck to a broken state at the beginning of a round. - Fixed inability to access the character tab in the tab menu when dead (preventing you from creating a new character). @@ -287,6 +121,7 @@ Fixes: - Fixed level floor not being visible on the sonar. - Fixed bots being unable to shoot with a turret whose line of sight is blocked by another turret (even though the projectiles can go through the turret). - Fixed switching a sub making its preview image disappear from the submarine switch menu. +- Fixed an issue where the client was adding mission rewards into the bank on their screen causing desync. - Fixed item assemblies still getting misaligned when saving. - Fixed crashing when there's no audio device available (no speakers/headset connected) and a character enters water. - Fixed crashing when trying to save an item assembly with a space at the end of the name. @@ -299,12 +134,72 @@ Fixes: - Signal components' and terminals' sprites don't mirror horizontally in mirrored subs (what's a DNA, RO, ROX or XEGER component??). - Fixed inability to rewire any docking ports in outpost levels, even if the port is not docked with anything (should only apply to the port docked with the outpost). - Fixed "Ignore This" orders being wiped when loading an existing multiplayer campaign save. +- Fixed abyss area being very small in the Aphotic Plateau, preventing the abyss monster from reaching you if you go deep enough. +- Fixed status monitor displaying small amounts of water as 1% even though water detectors output 0%. +- Fixed autopilot conflicting with VELOCITY_IN inputs (now signals override the autopilot for 1 second). +- Fixed ConversationAction getting interrupted when opening an input-blocking menu in single player. +- Fixed sprite bleed in chaingun ammunition boxes. +- Fixed appearance of specific named NPCs being inconsistent (e.g. Captain Hognose sometimes being a woman or not having an eyepatch). +- Fixed certain scripted events getting stuck if you switch characters in single player (e.g. the events that require you to interact with fliers on the wall). +- Fixed crashing when the source of a rope is removed (e.g. when a latcher despawns while latched on to the sub). +- Fixed votes always going through if no-one votes. +- Fixed energy drink giving x10 more haste when used via the health interface. +- Fixed the monster spawns for the new game plus not working (currently a placeholder set). +- Fixed monsters spawning from missions not avoiding the engines. +- Split campaign state networking messages into multiple ones. Previously all the campaign-related data (map state, reputation, upgrades, purchased items, selected missions) was included in the same message, and whenever anything in the data changed, the server would send all of it to clients. This would cause performance and bandwidth issues in some situations, for example when reputation was changing rapidly. +- Fixed some pumps in Kastrull working without power. +- Fixed quick-reloading working incorrectly when trying to reload from a stack that doesn't fully fit in the weapon (e.g. when double clicking on a full stack of revolver rounds with a half-loaded revolver in hand). +- Fixed inability to quick-reload weapons with more than 1 inventory slot (e.g. autoshotgun). +- Fixed outpost NPCs having x3 more health than they should. +- Fixed morbusine not killing NPCs with higher-than-default health. +- Fixed crashing with the error message "couldn't find a valid ICU package installed on the system" on some Linux distributions. +- Fixed graphics errors when using Razer Cortex overlay. +- Fixed bots being unable to repair Winterhalter's top hatch. +- Fixed server crashing if you disable all mission types and try to start a mission round. +- Fixed Chinese/Japanese/Korean text not wrapping properly on terminals. +- Fixed bots sometimes walking towards a wall or holding the ladders when they are idling. +- Fixed "main docking port" property not being taken into account when placing outposts (= the outpost was placed with the assumption that the docking port closest to the sub's center is the main docking port). Sometimes caused the outpost to be placed too close to the level walls, preventing the sub from docking with it. +- Fixed ladders not being visible in the sub preview. +- Fixed some UI elements being too large when switching from a large resolution to a smaller one, or vice versa. +- Fixed weapon holder sprite depth. +- Fixed level editor's test mode generating a different level than the editor itself. +- Fixed ballast flora branches that have been disconnected from the root not being considered disconnected after a level transition (allowing them to keep growing). +- Fixed "set default bindings" not doing anything in the settings menu. +- Fixed door/hatch gaps not getting moved when snapping to grid in the sub editor. +- Vertically mirrored beds can't be laid on. +- Fixed wrecked reactors being forced to non-interactable even if made interactable in the sub editor. +- Fixed keybinds shown in the controls tab not refreshing when resetting the binds. +- Hopefully fixed colonies sometimes not including some modules (most often the armory module). +- Fixed ready checks sometimes ending at a slightly different time client-side compared to the server, allowing you to answer the prompt even though the time to answer already ended server-side. +- Fixed large terminal welcome messages going slightly outside the bounds of the listbox. +- Fixed overlapping in the tab menu's mission tab when there's more than one mission selected. +- Fixed fabricators and deconstructors playing the sounds even if they're out of power. +- Fixed occasional "hash mismatch for downloaded mod" errors on Linux. +- Fixed clients occasionally spawning as the old character after they've opted to create a new one. Only happened if the client hadn't died and was still controlling the old character at the end of the round. +- When a client creates a character with a new name, the client's name is changed to match it after they spawn as that character. +- Fixed enabled mods getting disabled when updating them in the mods menu. +- Fixed a rounding error in Sprite.DrawTiled that sometimes caused an extra 1-pixel line on some scaled and flipped structures (e.g. certain wall pieces scaled to 0.6). +- Fixed Orca 2 still using the old chaingun charge time. Modding: +- Added "mod lists" which can be used to enable/disable sets of mods more easily. +- Option to choose which local mod(s) to add a submarine to when saving one in the submarine editor. +- Mods can be unsubscribed from by right-clicking on them in the mod list, and it's possible to unsubscribe from multiple ones at the same time by using ctrl+click or shift+click to select more than one. +- Local mods can be merged in the mod list by selecting the ones you want to merge and selecting "merge all selected" from the right-click context menu. +- Better filtering in the mod list: option to only show local mods, Workshop mods, published mods, submarines and/or item assemblies. +- Added "SameInventory" spawn position type to status effects (allows spawning items in the same inventory the entity applying the effect is in). +- Added support for multiple light components in wearables. - Fixed permanent stats given by talents not getting synced to clients in multiplayer (doesn't affect any vanilla talents). - Fixed nullref exception when trying to trigger a location type change to a type that doesn't exist (doesn't happen in the vanilla game). - Added an extra tag to the "canned heat" talent to make it easier to add custom upgradeable tanks that aren't compatible with vanilla tools. - Option to make status effects drop the items contained inside the target item (usage example in the duffel bag). +- Level object, cave and mineral commonness can be defined based on the biome instead of the level generation parameters (= no need to define commonness for "coldcavernsbasic", "coldcavernsmaze" etc separately). +- Option to define ConversationAction texts directly in the event xml (instead of having to always define them in a spearate text file). +- Extended CustomInterface functionality with NumberInput elements that allow using float values ("numbertype") and defining the increment size ("step") the number of decimal places ("decimalplaces"). (Thanks, mLuby!) +- Implemented element for removing all the child elements of an element in a variant file. +- TriggerComponent now supports negative forces: negative force value will cause the it to pull triggerers towards it. +- Multiple TriggerComponent properties can now be modified through signals and CustomInterface components. + --------------------------------------------------------------------------------------------------------- v0.17.16.0 From 71bd06f42574a39079c3a1645fe82e24d17eabfe Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Tue, 14 Jun 2022 12:11:33 +0300 Subject: [PATCH 12/14] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 2443947dc..c6b2f18fa 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -53,8 +53,8 @@ body: description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu. options: - 0.17.16.0 + - 0.18.10.0 (Unstable) - 0.18.9.0 (Unstable) - - 0.18.8.0 (Unstable) - Other validations: required: true From 8e6c601162d49b2bc3761a1092224087b4564e13 Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Thu, 16 Jun 2022 01:03:48 +0900 Subject: [PATCH 13/14] Build 0.18.11.0 --- .../GameModes/Tutorials/CaptainTutorial.cs | 2 +- .../GameModes/Tutorials/DoctorTutorial.cs | 2 +- .../GameModes/Tutorials/EngineerTutorial.cs | 2 +- .../GameModes/Tutorials/MechanicTutorial.cs | 2 +- .../GameModes/Tutorials/OfficerTutorial.cs | 2 +- .../Items/Components/Signal/ConnectionPanel.cs | 5 +++-- .../ClientSource/Settings/SettingsMenu.cs | 18 ++++++++++++------ Barotrauma/BarotraumaClient/LinuxClient.csproj | 3 +-- Barotrauma/BarotraumaClient/MacClient.csproj | 3 +-- .../BarotraumaClient/WindowsClient.csproj | 3 +-- Barotrauma/BarotraumaServer/LinuxServer.csproj | 3 +-- Barotrauma/BarotraumaServer/MacServer.csproj | 3 +-- .../BarotraumaServer/WindowsServer.csproj | 3 +-- Barotrauma/BarotraumaShared/changelog.txt | 16 +++++++++++++++- 14 files changed, 41 insertions(+), 26 deletions(-) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs index c4ec80301..76cb87ce0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs @@ -104,7 +104,7 @@ namespace Barotrauma.Tutorials foreach (Item item in captain.Inventory.AllItemsMod) { - if (item.HasTag("identitycard") || item.HasTag("headset")) { continue; } + if (item.HasTag("identitycard") || item.HasTag("mobileradio")) { continue; } item.Unequip(captain); captain.Inventory.RemoveItem(item); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs index ff73b2f00..41a8238bd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs @@ -107,7 +107,7 @@ namespace Barotrauma.Tutorials foreach (Item item in doctor.Inventory.AllItemsMod) { - if (item.HasTag("clothing") || item.HasTag("identitycard") || item.HasTag("headset")) { continue; } + if (item.HasTag("clothing") || item.HasTag("identitycard") || item.HasTag("mobileradio")) { continue; } item.Unequip(doctor); doctor.Inventory.RemoveItem(item); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs index c30c1c346..b95fa6f31 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs @@ -133,7 +133,7 @@ namespace Barotrauma.Tutorials foreach (Item item in engineer.Inventory.AllItemsMod) { - if (item.HasTag("clothing") || item.HasTag("identitycard") || item.HasTag("headset")) { continue; } + if (item.HasTag("clothing") || item.HasTag("identitycard") || item.HasTag("mobileradio")) { continue; } item.Unequip(engineer); engineer.Inventory.RemoveItem(item); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs index 23b5da096..06d05b708 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs @@ -162,7 +162,7 @@ namespace Barotrauma.Tutorials foreach (Item item in mechanic.Inventory.AllItemsMod) { - if (item.HasTag("clothing") || item.HasTag("identitycard") || item.HasTag("headset")) { continue; } + if (item.HasTag("clothing") || item.HasTag("identitycard") || item.HasTag("mobileradio")) { continue; } item.Unequip(mechanic); mechanic.Inventory.RemoveItem(item); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs index a8ba18da8..dd0f49fe5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs @@ -143,7 +143,7 @@ namespace Barotrauma.Tutorials foreach (Item item in officer.Inventory.AllItemsMod) { - if (item.HasTag("clothing") || item.HasTag("identitycard") || item.HasTag("headset")) { continue; } + if (item.HasTag("clothing") || item.HasTag("identitycard") || item.HasTag("mobileradio")) { continue; } item.Unequip(officer); officer.Inventory.RemoveItem(item); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs index 21b3b4204..541c54a63 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs @@ -147,9 +147,10 @@ namespace Barotrauma.Items.Components //because some of the wires connected to the panel may not exist yet long msgStartPos = msg.BitPosition; msg.ReadUInt16(); //user ID - foreach (Connection connection in Connections) + foreach (Connection _ in Connections) { - for (int i = 0; i < connection.MaxWires; i++) + uint wireCount = msg.ReadVariableUInt32(); + for (int i = 0; i < wireCount; i++) { msg.ReadUInt16(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index 50edb5235..4e9d8b6a6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -462,6 +462,8 @@ namespace Barotrauma Slider(voiceChat, (0, 500), 26, (v) => $"{Round(v)} ms", unsavedConfig.Audio.VoiceChatCutoffPrevention, (v) => unsavedConfig.Audio.VoiceChatCutoffPrevention = Round(v), TextManager.Get("CutoffPreventionTooltip")); } + + private bool inputBoxSelectedThisFrame = false; private void CreateControlsTab() { GUIFrame content = CreateNewContentFrame(Tab.Controls); @@ -510,16 +512,13 @@ namespace Barotrauma bool willBeSelected = !btn.Selected; if (willBeSelected) { + inputBoxSelectedThisFrame = true; currentSetter = (v) => { valueSetter(v); btn.Text = valueNameGetter(); }; } - else - { - currentSetter = null; - } btn.Selected = willBeSelected; return true; @@ -538,6 +537,12 @@ namespace Barotrauma { if (currentSetter is null) { return; } + if (PlayerInput.PrimaryMouseButtonClicked() && inputBoxSelectedThisFrame) + { + inputBoxSelectedThisFrame = false; + return; + } + void clearSetter() { currentSetter = null; @@ -551,7 +556,7 @@ namespace Barotrauma } var pressedKeys = PlayerInput.GetKeyboardState.GetPressedKeys(); - if ((pressedKeys?.Any() ?? false)) + if (pressedKeys?.Any() ?? false) { if (pressedKeys.Contains(Keys.Escape)) { @@ -562,7 +567,8 @@ namespace Barotrauma callSetter(pressedKeys.First()); } } - else if (PlayerInput.PrimaryMouseButtonClicked() && !(GUI.MouseOn is GUIButton)) + else if (PlayerInput.PrimaryMouseButtonClicked() && + (GUI.MouseOn == null || !(GUI.MouseOn is GUIButton) || GUI.MouseOn.IsChildOf(keyMapList.Content))) { callSetter(MouseButton.PrimaryMouse); } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index dcb6b28f4..1f9b0f11c 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,13 +6,12 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.9.0 + 0.18.11.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico Debug;Release;Unstable - true ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index dfc188b15..630273d43 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,13 +6,12 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.9.0 + 0.18.11.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico Debug;Release;Unstable - true ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 8043dbfaf..26662833d 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,14 +6,13 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.9.0 + 0.18.11.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico Debug;Release;Unstable app.manifest - true ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 26ca8d0d6..edd4b10ac 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,13 +6,12 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.9.0 + 0.18.11.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable - true ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 883360337..d0e34c416 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,13 +6,12 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.9.0 + 0.18.11.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable - true ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index e1bf7a611..6210c5922 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,13 +6,12 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.9.0 + 0.18.11.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable - true ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 58186d997..bc2dec55c 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,18 @@ +--------------------------------------------------------------------------------------------------------- +v0.18.11.0 +--------------------------------------------------------------------------------------------------------- + +- Disabled project-wide invariant globalization, which was meant to address "couldn't find a valid ICU package installed on the system" errors on some Linux distributions. The fix caused issues with case-insensitive comparisons and converting to upper or lower case in non-latin alphabets. +- Fixed tutorial characters spawning without a headset. +- Fixed inability to bind keys to LMB by clicking on the input box. + +--------------------------------------------------------------------------------------------------------- +v0.18.10.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed "index out of range" error when joining a server where's someone's rewired items. +- Fixed some mission messages being displayed as "missionmessage0.nameofthemission". + --------------------------------------------------------------------------------------------------------- v0.18.9.0 --------------------------------------------------------------------------------------------------------- @@ -152,7 +167,6 @@ Fixes: - Fixed inability to quick-reload weapons with more than 1 inventory slot (e.g. autoshotgun). - Fixed outpost NPCs having x3 more health than they should. - Fixed morbusine not killing NPCs with higher-than-default health. -- Fixed crashing with the error message "couldn't find a valid ICU package installed on the system" on some Linux distributions. - Fixed graphics errors when using Razer Cortex overlay. - Fixed bots being unable to repair Winterhalter's top hatch. - Fixed server crashing if you disable all mission types and try to start a mission round. From 08cc2faec8de939b409fe256bb2e19d99cbd0968 Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Wed, 15 Jun 2022 19:11:29 +0300 Subject: [PATCH 14/14] Removed old issue report template --- .github/ISSUE_TEMPLATE/bug_report.md | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index b66371762..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: Bug report (old) -about: Please use the other report form to report issues. This old template is only here until we've replaced the bug report links in the game. -title: '' -labels: '' -assignees: '' - ---- - -Please use the other report form to report issues. This old template is only here until we've replaced the bug report links in the game. - -**Description** -A clear and concise description of what the bug is. - -**Steps To Reproduce** -If possible, describe how the developers can get the bug to happen (for example, "the game crashes when I try to put handcuffs on a Moloch"). Please also mention whether the bug happened in a multiplayer or single player session. It is often extremely hard to fix a bug if we don't know how to reproduce it. - -**Version** -Which version of the game did the bug happen in. Also, please include the operating system you're using (Windows/Linux/Mac). - -**Additional information** -Add any other context about the problem here. If the bug always occurs in a specific save file or submarine, attaching the file to the report makes it much easier for us to diagnose. Since GitHub doesn't allow attaching the .sub or .save files to reports directly, you need to .zip them first.